| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
…ents
When a named function is passed as a callback argument — e.g.
`router.use(handleToken)`, `arr.map(transform)`, or
`promise.then(onSuccess)` — the extractor now creates a dynamic call
edge to the referenced function. Previously only the method call itself
(e.g. `router.use`) was recorded, leaving the handler with zero callers
and a `dead-unresolved` classification.
Additionally, destructured `const` bindings such as
`const { handleToken, checkPermissions } = initAuth(config)` are now
emitted as function definitions so the edge resolver can match them as
call targets.
Both the query-based and walk-based extraction paths are covered.
Tested on a 1 895-file / 10-service Express + TypeScript monorepo:
- Before: handleToken 0 callers, checkPermissions 0 callers
- After: handleToken 21 callers across 6 services,
checkPermissions 18 callers across 3 services
- Graph edges: 29 465 → 30 768 (+1 303)
- Graph nodes: 18 674 → 18 984 (+310 from destructured bindings)
|
All contributors have signed the CLA ✍️ ✅ |
Sorry, something went wrong.
Greptile SummaryThis PR adds two complementary features to the JS/TS extractor: named function references passed as callback arguments now emit dynamic: true call edges, and const-destructured object bindings emit synthetic function definitions. The implementation is consistent across all three execution paths (TS query, TS walk, and Rust native), with correct const-only and function-scope guards throughout.
Confidence Score: 4/5Safe to merge once the ci-pipeline needs list is patched; extraction logic and parity are correct. All prior P0/P1 parity and guard concerns from earlier rounds are resolved. The one remaining P1 is a CI integrity gap — native build failures won't block merges — which is a one-line fix and doesn't affect the runtime behavior of the feature itself. .github/workflows/ci.yml — ci-pipeline needs list must include native-host-build. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[call_expression node] --> B{fn.type == import?}
B -- yes --> C[handle_dynamic_import\nskip callback extraction]
B -- no --> D[extractCallInfo\nemit static call]
D --> E[extractCallbackDefinition\nemit anon fn def if member expr]
E --> F[extractCallbackReferenceCalls\nfor each identifier/member arg\nemit dynamic call edge]
G[const declarator] --> H{nameN.type?}
H -- identifier --> I[existing paths:\narrow fn / literal constant]
H -- object_pattern --> J{is_const AND\nno function-scope ancestor?}
J -- no --> K[skip]
J -- yes --> L[extractDestructuredBindings\neach property -> kind:function definition]
Comments Outside Diff (1)
Reviews (12): Last reviewed commit: "refactor(ci): share native build between..." | Re-trigger Greptile |
Sorry, something went wrong.
| } else if (nameN.type === 'object_pattern') { | ||
| // Destructured bindings: const { handleToken, checkPermissions } = initAuth(...) | ||
| // Each destructured property becomes a function definition so it can be | ||
| // resolved when passed as a callback (e.g. router.use(handleToken)) | ||
| extractDestructuredBindings( | ||
| nameN, | ||
| node.startPosition.row + 1, | ||
| nodeEndLine(node), | ||
| ctx.definitions, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Missing const guard for destructured binding extraction
The new object_pattern branch emits a kind: 'function' definition for every destructured binding regardless of whether the declaration uses const, let, or var. The existing constant-extraction branch immediately above it (line 741) correctly guards on isConst. A let { userId, email } = parseRequest(req) would emit userId and email as function definitions, causing spurious dynamic call edges (e.g. if userId is later passed as an argument anywhere). The same gap exists in extractDestructuredBindingsWalk, which processes all lexical_declaration and variable_declaration nodes without restricting to const — its own JSDoc says "const bindings".
Suggestion for this path:
| } else if (nameN.type === 'object_pattern') { | |
| // Destructured bindings: const { handleToken, checkPermissions } = initAuth(...) | |
| // Each destructured property becomes a function definition so it can be | |
| // resolved when passed as a callback (e.g. router.use(handleToken)) | |
| extractDestructuredBindings( | |
| nameN, | |
| node.startPosition.row + 1, | |
| nodeEndLine(node), | |
| ctx.definitions, | |
| ); | |
| } | |
| } else if (isConst && nameN.type === 'object_pattern') { | |
| // Destructured bindings: const { handleToken, checkPermissions } = initAuth(...) | |
| // Each destructured property becomes a function definition so it can be | |
| // resolved when passed as a callback (e.g. router.use(handleToken)) | |
| extractDestructuredBindings( | |
| nameN, | |
| node.startPosition.row + 1, | |
| nodeEndLine(node), | |
| ctx.definitions, | |
| ); |
And for extractDestructuredBindingsWalk, add a guard analogous to extractConstDeclarators:
if (t === 'lexical_declaration' || t === 'variable_declaration') {
if (!declNode.text.startsWith('const ')) { /* skip, fall through to recurse */ }
else { /* existing declarator loop */ }
}
Sorry, something went wrong.
There was a problem hiding this comment.
Fixed — added isConst guard on the object_pattern branch in handleVariableDecl (line 751) and a startsWith('const ') check in extractDestructuredBindingsWalk (lines 391 and 418). The Rust port in crates/codegraph-core/src/extractors/javascript.rs applies the same is_const guard (line 283). Test added in tests/parsers/javascript.test.ts verifies let and var destructured bindings are not emitted as definitions.
Sorry, something went wrong.
|
I have read the CLA Document and I hereby sign the CLA |
Sorry, something went wrong.
|
recheck |
Sorry, something went wrong.
Review feedback from Greptile: the previous implementation emitted
function definitions for destructured bindings regardless of whether
the declaration used `const`, `let`, or `var`. A transient binding
like `let { userId } = parseRequest(req)` would be treated as a stable
function reference, producing spurious dynamic call edges if `userId`
was later passed as an argument.
Restrict extraction to `const` declarations, matching the semantics
of a stable re-export pattern. Same guard applied in both the query
path (`handleVariableDecl`) and the walk path (`extractDestructuredBindingsWalk`).
|
Thanks for the review. Pushed a fix that restricts destructured binding extraction to const declarations only, in both the query and walk paths, and added a test verifying that let and var destructured bindings are not emitted as definitions. |
Sorry, something went wrong.
Sorry, something went wrong.
|
Claude encountered an error —— View job Failed with exit code 128 I'll analyze this and get back to you. |
Sorry, something went wrong.
…g extraction
Port two JS extractor features from WASM/TS to the Rust native engine
for engine parity:
1. Callback reference extraction — identifier and member_expression
arguments in call expressions are emitted as dynamic call edges
(e.g. router.use(handleToken) produces a call to handleToken)
2. Destructured const binding extraction — const { a, b } = init()
creates function definitions for each destructured property,
restricted to const (not let/var)
Impact: 15 functions changed, 3 affected
|
Pushed a commit to port the two JS extractor features to the Rust native engine for engine parity: extract_callback_reference_calls — When processing call expressions, identifier and member_expression arguments are now emitted as dynamic call edges (e.g. router.use(handleToken) → call to handleToken; app.use(auth.validate) → call to validate with receiver auth). Wired into handle_call_expr. extract_destructured_bindings — const { handleToken, checkPermissions } = initAuth() now creates function definitions for each destructured property. Handles shorthand ({ foo }), renamed ({ original: renamed }), and is restricted to const only (not let/var). Wired into handle_var_decl. Added 11 Rust tests covering both features (callback references, member expressions, multiple args, no false positives for non-identifiers, destructured bindings, exported destructured bindings, let/var skip, renamed bindings). Library compiles cleanly (cargo check passes). The pre-existing test compilation failures in other extractor files (edge_builder.rs, bash.rs, c.rs, kotlin.rs, swift.rs) are unrelated. |
Sorry, something went wrong.
|
The previous CI failures:
|
Sorry, something went wrong.
| if let Some(cb_def) = extract_callback_definition(node, source) { | ||
| symbols.definitions.push(cb_def); | ||
| } | ||
| extract_callback_reference_calls(node, source, &mut symbols.calls); |
There was a problem hiding this comment.
Engine parity divergence: extract_callback_reference_calls runs on import() nodes
extract_callback_reference_calls is called unconditionally here, but the TypeScript walk-path equivalent (handleCallExpr) only calls extractCallbackReferenceCalls in the else branch — i.e., it is skipped entirely when fn.type === 'import'. For import(modulePath) (variable module path), the Rust engine emits a spurious dynamic call to modulePath that neither the TypeScript walk path nor the query path would produce.
Per the project's dual-engine policy, both engines must produce identical results. The fix is to guard this call the same way the TypeScript walk path does:
fn handle_call_expr(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
if let Some(fn_node) = node.child_by_field_name("function") {
if fn_node.kind() == "import" {
handle_dynamic_import(node, &fn_node, source, symbols);
} else {
if let Some(call_info) = extract_call_info(&fn_node, node, source) {
symbols.calls.push(call_info);
}
if let Some(cb_def) = extract_callback_definition(node, source) {
symbols.definitions.push(cb_def);
}
extract_callback_reference_calls(node, source, &mut symbols.calls);
}
}
}This also brings extract_callback_definition in line with TypeScript (currently it is called unconditionally for import nodes in Rust too, but that's a pre-existing gap).
Sorry, something went wrong.
There was a problem hiding this comment.
Fixed — restructured handle_call_expr in crates/codegraph-core/src/extractors/javascript.rs to early-return after handle_dynamic_import, so extract_callback_reference_calls and extract_callback_definition only run in the non-import branch (mirroring the TS walk path). Added unit test no_dynamic_call_for_dynamic_import_arg asserting import(modulePath) does not emit a spurious dynamic call to modulePath. Pushed in commit 64cb5b8.
Sorry, something went wrong.
…port() (optave#947) The Rust handle_call_expr was calling extract_callback_reference_calls unconditionally, including when fn.kind() == "import". The TS walk-path equivalent (handleCallExpr) only runs callback-reference extraction in the else branch, so import(modulePath) emitted a spurious dynamic call to modulePath in the Rust engine but not in TS — violating dual-engine parity. Restructured handle_call_expr to early-return after handle_dynamic_import, mirroring the TS if/else. Added unit test asserting import() arguments are not emitted as dynamic calls. Impact: 2 functions changed, 1 affected
…ution' into feat/callback-reference-resolution Impact: 14 functions changed, 0 affected
Sorry, something went wrong.
…ave#947) The Rust suite includes extracts_renamed_destructured_binding covering const { original: renamed } = initAuth() — but the TS side had no equivalent test. If the pair_pattern branch in extractDestructuredBindings were accidentally broken, none of the existing TS tests would catch it. Mirrors the Rust test: asserts the local alias is emitted as a function definition and the original property name is not.
|
Addressed the remaining P2 — added extracts renamed destructured const binding under its local alias in tests/parsers/javascript.test.ts, mirroring the Rust-side extracts_renamed_destructured_binding test. All 44 TS JS parser tests pass locally. |
Sorry, something went wrong.
| } else if is_const && name_n.kind() == "object_pattern" { | ||
| extract_destructured_bindings(&name_n, source, start_line(node), end_line(node), &mut symbols.definitions); |
There was a problem hiding this comment.
Engine parity divergence: missing find_parent_of_types scope guard
The object_pattern branch emits destructured bindings from const declarations anywhere in the file, including inside function bodies. The constant-extraction branch immediately below it (line 285) uses find_parent_of_types to restrict to non-function scope, and the TS query path's extractDestructuredBindingsWalk skips FUNCTION_SCOPE_TYPES for the same reason. Per the project's dual-engine policy, both engines must produce identical results.
For function setup() { const { handleToken } = initAuth(config); }:
| } else if is_const && name_n.kind() == "object_pattern" { | |
| extract_destructured_bindings(&name_n, source, start_line(node), end_line(node), &mut symbols.definitions); | |
| } else if is_const && name_n.kind() == "object_pattern" | |
| && find_parent_of_types(node, &[ | |
| "function_declaration", "arrow_function", | |
| "function_expression", "method_definition", | |
| "generator_function_declaration", "generator_function", | |
| ]).is_none() | |
| { | |
| extract_destructured_bindings(&name_n, source, start_line(node), end_line(node), &mut symbols.definitions); |
Sorry, something went wrong.
There was a problem hiding this comment.
Fixed — added find_parent_of_types scope guard to the object_pattern branch in handle_var_decl (Rust), mirroring the guard already on the constant-extraction branch. Also added unit test skips_destructured_bindings_inside_function_scope asserting function setup() { const { handleToken } = initAuth(); } does not emit handleToken as a definition. Pushed in commit 7711ffd.
Sorry, something went wrong.
optave#947) The Rust walk path's handle_var_decl emitted destructured const bindings from any scope — including inside function bodies — while the TS query path's extractDestructuredBindingsWalk skips FUNCTION_SCOPE_TYPES. For function setup() { const { handleToken } = initAuth(); } the Rust engine emitted handleToken as a definition but the TS WASM engine did not, violating the dual-engine parity requirement. Added find_parent_of_types guard to the object_pattern branch, mirroring the guard already present on the constant-extraction branch. Added unit test skips_destructured_bindings_inside_function_scope asserting the parity. Impact: 2 functions changed, 1 affected
Sorry, something went wrong.
| } else if (isConst && nameN.type === 'object_pattern') { | ||
| // Destructured bindings: const { handleToken, checkPermissions } = initAuth(...) | ||
| // Each destructured property becomes a function definition so it can be | ||
| // resolved when passed as a callback (e.g. router.use(handleToken)). | ||
| // Restricted to const to avoid creating spurious definitions for | ||
| // transient let/var destructuring (e.g. let { userId } = parseRequest(req)). | ||
| extractDestructuredBindings( | ||
| nameN, | ||
| node.startPosition.row + 1, | ||
| nodeEndLine(node), | ||
| ctx.definitions, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Walk-path missing function-scope guard for object_pattern
walkJavaScriptNode descends unconditionally into function bodies, so this branch fires for const { handler } = initAuth() declared anywhere in the file — including inside function bodies. The query path prevents this via FUNCTION_SCOPE_TYPES.has(child.type) continue in extractDestructuredBindingsWalk, and the Rust path was explicitly fixed in this PR with find_parent_of_types. The walk path is the only one left unguarded.
FUNCTION_SCOPE_TYPES is already in scope and node.parent is available on tree-sitter nodes, so the fix is a parent-walk check:
} else if (isConst && nameN.type === 'object_pattern') {
// Guard: skip bindings inside function scopes, matching extractDestructuredBindingsWalk
// (query path) and handle_var_decl (Rust path).
let p: typeof node | null = node.parent ?? null;
while (p && !FUNCTION_SCOPE_TYPES.has(p.type)) p = p.parent ?? null;
if (!p) {
extractDestructuredBindings(
nameN,
node.startPosition.row + 1,
nodeEndLine(node),
ctx.definitions,
);
}
Sorry, something went wrong.
There was a problem hiding this comment.
Fixed — added hasFunctionScopeAncestor helper in src/extractors/javascript.ts and gated the walk-path object_pattern branch on it, mirroring the find_parent_of_types guard in the Rust handle_var_decl. Added test does not extract destructured bindings declared inside function scope in tests/parsers/javascript.test.ts asserting function setup() { const { handleToken } = initAuth(); } does not emit handleToken as a definition. Pushed in commit bf32580.
Sorry, something went wrong.
…on scope (optave#947) The walk-path handleVariableDecl emitted destructured const bindings from any scope — including inside function bodies — while the query-path extractDestructuredBindingsWalk already skips FUNCTION_SCOPE_TYPES. When the walk path is used as fallback, function-internal const destructurings were incorrectly registered as definitions, diverging from the query path. Added hasFunctionScopeAncestor helper and gated the object_pattern branch on it, mirroring the Rust handle_var_decl find_parent_of_types guard. Added regression test asserting function-internal destructured bindings are not emitted. Impact: 2 functions changed, 4 affected
Sorry, something went wrong.
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
Sorry, something went wrong.
The test and parity CI jobs were running against the last-published native binary (@optave/codegraph-*@3.9.3) installed via npm. When a PR includes Rust extractor changes, the WASM side picks up the new TS changes while the native side stays on the old published binary, producing false parity failures. Add a `scripts/ci-rebuild-native.mjs` helper that runs `napi build --release` and copies the resulting .node file over the published binary in node_modules/@optave/<platform-pkg>/. Wire it into the `test` and `parity` jobs after `npm install` so parity is compared against the Rust source under review. Also reformat the destructured-binding guard in src/extractors/javascript.ts to a single line to satisfy biome (the lint job was failing on this).
Extract the napi build into a dedicated `native-host-build` matrix job that uploads the `.node` per OS as an artifact. `test` and `parity` download that artifact and install it over the published platform binary. Replaces the per-job rebuild (compiled Rust twice) with one build shared by both downstream jobs. `ci-rebuild-native.mjs` becomes `ci-install-native.mjs` — copy-only, no build invocation.
Without this, a Rust compile failure in native-host-build causes test and parity to be skipped (not failed), which the ci-pipeline check treats as success since it only matches 'failure' and 'cancelled'. Adding native-host-build to the needs list propagates its failure directly so the required status check correctly turns red.
|
Addressed Greptile's outstanding P1 (ci-pipeline doesn't gate on native-host-build failure):
Pushed in commit ae05f74. Re: the failing impact check — that's the fork-PR GITHUB_TOKEN permission issue (403 Resource not accessible by integration when posting the comment). PR #951 is the dedicated fix (splits the workflow into analysis + workflow_run-triggered comment job, the GitHub-recommended pattern for fork-safe PR comments). Out of scope for this PR. |
Sorry, something went wrong.
|
@meirLixen thank you for your contribution. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Closes #946
Summary
Two changes to the JS/TS extractor that work together to resolve named function references passed as callback arguments:
Callback reference extraction — When processing a call expression, identifier and member_expression arguments are now emitted as dynamic call edges. router.use(handleToken) produces calls to both use and handleToken. Covers the query-based path (dispatchQueryMatch), the walk-based path (handleCallExpr), and plain function calls (callfn_node).
Destructured binding definitions — const { handleToken, checkPermissions } = initAuth(config) now emits each destructured property as a function definition, so the edge resolver can match it as a call target. Covers both the walk-based path (handleVariableDecl) and the query-based path (via a new extractDestructuredBindingsWalk post-processing step, following the same pattern as extractConstantsWalk).
Motivation
Express middleware, event emitters, array higher-order functions (.map, .filter), Promise chains (.then, .catch), and setTimeout/setInterval all pass functions by reference. Without this change, every such handler appears as dead-unresolved with zero callers.
Tested on a 1 895-file Express + TypeScript monorepo (10 microservices, 8 shared libraries):
Design decisions
Test plan