[GH-ISSUE #3885] [Bug] Optional require call loses its native-addon binding record #1505

Closed
opened 2026-10-16 21:06:17 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @coygeek on GitHub (Oct 15, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3885

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

In a CommonJS file, changing a native-addon request from require('./addon.node') to the optional form require?.('./addon.node') removes the native-addon binding, the native-export node, and both native binding counts, even though the request targets the same addon and the ordinary dependency is still recognized.

The ordinary-call control reports one resolved native binding and one native-export node; changing only the call to optional syntax makes both native binding counts zero and removes that node.

Both results still contain the same resolved loads edge for ./addon.node, so the optional call and target are recognized elsewhere in the same result.

A direct call to the built analyzeJavaScriptStaticSource also returned parse_status: "complete" and a literal require reference for both forms, but electron.native_addon_bindings was empty only for the optional call.

Reproduction

Run from a built source checkout (the probe invokes the CLI through scripts/rea.mjs). Create two disposable directories, each with an addon.node holding the bytes fixture bytes, never executed with no trailing newline, and these exact main.cjs inputs:

ordinary="$(mktemp -d)"; optional="$(mktemp -d)"
printf "const addon = require('./addon.node');\n" > "$ordinary/main.cjs"
printf "const addon = require?.('./addon.node');\n" > "$optional/main.cjs"
printf 'fixture bytes, never executed' > "$ordinary/addon.node"
printf 'fixture bytes, never executed' > "$optional/addon.node"
nice -n 10 node scripts/rea.mjs analyze-javascript-application "$ordinary" --format json > "$ordinary/out.json"
nice -n 10 node scripts/rea.mjs analyze-javascript-application "$optional" --format json > "$optional/out.json"
for f in "$ordinary/out.json" "$optional/out.json"; do
  jq '{native_addon_bindings:.normalized_result.summary.native_addon_bindings,
       resolved_native_addon_bindings:.normalized_result.summary.resolved_native_addon_bindings,
       native_export_nodes:[.normalized_result.graph.nodes[]|select(.kind=="native-export")]|length}' "$f"
done

Expected behavior

docs/javascript-artifact-reconstruction.md:241 promises native .node binding requests without executing the addon, and src/domain/javascript/electronStaticAnalysisTypes.ts:176 models a requested binding including namespace access.

In a CommonJS file an unshadowed require is present, so require?.('./addon.node') requests the same addon namespace as require('./addon.node').

The owning source traversal explicitly supports optional calls at src/domain/javascript/javascriptStaticAnalysis.ts:241, and ordinary dependency collection recognizes and resolves this exact optional request.

Impact and scope

CommonJS applications using optional require syntax lose the native binding layer and its counts despite retaining the underlying dependency; the demonstrated namespace binding is omitted and a direct built-parser probe also established the node-kind mismatch.

Workaround: available. analysts can inspect the retained resolved dependency and source to recover the request manually; no option that restores the missing native binding node while preserving the input was verified.

Reporter's impact estimate: P2 (supported).

REA version

6.5.0, built from source at main commit 034342ba28 (latest npm release is 6.5.0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1 arm64

Target and artifact context

CommonJS Electron main source with a placeholder .node addon

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: docs/javascript-artifact-reconstruction.md:241; requested-binding type at src/domain/javascript/electronStaticAnalysisTypes.ts:176; optional-call support at src/domain/javascript/javascriptStaticAnalysis.ts:241
  • Failure source: observed zero native bindings for the optional form while the ordinary control retained one (output below); static cause at src/domain/javascript/electronStaticAnalysisNative.ts:220, where nativeRequire rejects any terminal node that is not a Babel CallExpression, so an OptionalCallExpression returns before callee or specifier classification
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete
ordinary:
  native_addon_bindings: 1
  resolved_native_addon_bindings: 1
  native-export nodes: 1
optional:
  native_addon_bindings: 0
  resolved_native_addon_bindings: 0
  native-export nodes: 0
both retain:
  {"relation":"loads","kind":"require","specifier":"./addon.node",
   "expression":null,"resolution_mode":"require",
   "resolved_path":"addon.node","resolution_status":"resolved"}

Typed outcome or recovery expected

require?.('./addon.node') should produce the same native-addon binding, native-export node, and binding counts as require('./addon.node'); native_addon_bindings should be 1.

Restoration check:

Failing: the optional-call input reports native_addon_bindings: 0, resolved_native_addon_bindings: 0, and no native-export node.

Passing: the optional-call input reports the same one resolved native binding and one native-export node as the ordinary-call control, while both inputs continue to retain the resolved loads dependency edge.

Analysis or suspected cause

electronStaticAnalysisNative.ts:220 rejects any terminal node that is not a Babel CallExpression, so an OptionalCallExpression returns before callee/specifier classification and the finding never reaches ElectronBoundaryGraphNative.ts:34 or the counts at ElectronBoundaryAnalysis.ts:171, while the ordinary reference path still accepts optional calls via javascriptStaticAnalysis.ts:241.

Additional context

The missing native finding never reaches src/application/javascript/ElectronBoundaryGraphNative.ts:34 or the summary counts at src/application/javascript/ElectronBoundaryAnalysis.ts:171.

Related issues: closed #2883 concerns the same optional-call omission in fetched source-map dependency extraction for analyze_web_bundle, not the Electron native binding collector used here; closed #822 concerns computed member keys and does not cover optional calls. No exact open duplicate found.

This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3885 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior In a CommonJS file, changing a native-addon request from `require('./addon.node')` to the optional form `require?.('./addon.node')` removes the native-addon binding, the `native-export` node, and both native binding counts, even though the request targets the same addon and the ordinary dependency is still recognized. The ordinary-call control reports one resolved native binding and one `native-export` node; changing only the call to optional syntax makes both native binding counts zero and removes that node. Both results still contain the same resolved `loads` edge for `./addon.node`, so the optional call and target are recognized elsewhere in the same result. A direct call to the built `analyzeJavaScriptStaticSource` also returned `parse_status: "complete"` and a literal `require` reference for both forms, but `electron.native_addon_bindings` was empty only for the optional call. ### Reproduction Run from a built source checkout (the probe invokes the CLI through `scripts/rea.mjs`). Create two disposable directories, each with an `addon.node` holding the bytes `fixture bytes, never executed` with no trailing newline, and these exact `main.cjs` inputs: ```sh ordinary="$(mktemp -d)"; optional="$(mktemp -d)" printf "const addon = require('./addon.node');\n" > "$ordinary/main.cjs" printf "const addon = require?.('./addon.node');\n" > "$optional/main.cjs" printf 'fixture bytes, never executed' > "$ordinary/addon.node" printf 'fixture bytes, never executed' > "$optional/addon.node" nice -n 10 node scripts/rea.mjs analyze-javascript-application "$ordinary" --format json > "$ordinary/out.json" nice -n 10 node scripts/rea.mjs analyze-javascript-application "$optional" --format json > "$optional/out.json" for f in "$ordinary/out.json" "$optional/out.json"; do jq '{native_addon_bindings:.normalized_result.summary.native_addon_bindings, resolved_native_addon_bindings:.normalized_result.summary.resolved_native_addon_bindings, native_export_nodes:[.normalized_result.graph.nodes[]|select(.kind=="native-export")]|length}' "$f" done ``` ### Expected behavior docs/javascript-artifact-reconstruction.md:241 promises native `.node` binding requests without executing the addon, and src/domain/javascript/electronStaticAnalysisTypes.ts:176 models a requested binding including namespace access. In a CommonJS file an unshadowed `require` is present, so `require?.('./addon.node')` requests the same addon namespace as `require('./addon.node')`. The owning source traversal explicitly supports optional calls at src/domain/javascript/javascriptStaticAnalysis.ts:241, and ordinary dependency collection recognizes and resolves this exact optional request. ### Impact and scope CommonJS applications using optional `require` syntax lose the native binding layer and its counts despite retaining the underlying dependency; the demonstrated namespace binding is omitted and a direct built-parser probe also established the node-kind mismatch. Workaround: available. analysts can inspect the retained resolved dependency and source to recover the request manually; no option that restores the missing native binding node while preserving the input was verified. Reporter's impact estimate: P2 (supported). ### REA version 6.5.0, built from source at main commit 034342ba28c8e56c3746be92b78aa7299afa02bd (latest npm release is 6.5.0) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1 arm64 ### Target and artifact context CommonJS Electron main source with a placeholder .node addon ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence - Expected source: docs/javascript-artifact-reconstruction.md:241; requested-binding type at src/domain/javascript/electronStaticAnalysisTypes.ts:176; optional-call support at src/domain/javascript/javascriptStaticAnalysis.ts:241 - Failure source: observed zero native bindings for the optional form while the ordinary control retained one (output below); static cause at src/domain/javascript/electronStaticAnalysisNative.ts:220, where `nativeRequire` rejects any terminal node that is not a Babel `CallExpression`, so an `OptionalCallExpression` returns before callee or specifier classification - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete ```text ordinary: native_addon_bindings: 1 resolved_native_addon_bindings: 1 native-export nodes: 1 optional: native_addon_bindings: 0 resolved_native_addon_bindings: 0 native-export nodes: 0 both retain: {"relation":"loads","kind":"require","specifier":"./addon.node", "expression":null,"resolution_mode":"require", "resolved_path":"addon.node","resolution_status":"resolved"} ``` ### Typed outcome or recovery expected require?.('./addon.node') should produce the same native-addon binding, native-export node, and binding counts as require('./addon.node'); native_addon_bindings should be 1. Restoration check: Failing: the optional-call input reports `native_addon_bindings: 0`, `resolved_native_addon_bindings: 0`, and no `native-export` node. Passing: the optional-call input reports the same one resolved native binding and one `native-export` node as the ordinary-call control, while both inputs continue to retain the resolved `loads` dependency edge. ### Analysis or suspected cause electronStaticAnalysisNative.ts:220 rejects any terminal node that is not a Babel CallExpression, so an OptionalCallExpression returns before callee/specifier classification and the finding never reaches ElectronBoundaryGraphNative.ts:34 or the counts at ElectronBoundaryAnalysis.ts:171, while the ordinary reference path still accepts optional calls via javascriptStaticAnalysis.ts:241. ### Additional context The missing native finding never reaches src/application/javascript/ElectronBoundaryGraphNative.ts:34 or the summary counts at src/application/javascript/ElectronBoundaryAnalysis.ts:171. Related issues: closed #2883 concerns the same optional-call omission in fetched source-map dependency extraction for `analyze_web_bundle`, not the Electron native binding collector used here; closed #822 concerns computed member keys and does not cover optional calls. No exact open duplicate found. This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.
Author
Owner

@matbrgz commented on GitHub (Oct 16, 2026):

Cross-referencing with sibling issue #3884 ([Bug] Local require method creates a resolved native-addon binding). Both issues pinpoint the AST extraction of native-addon bindings in analyze_javascript_application (handling method shadowing vs optional/conditional require branches).

Consolidated under Cluster M (Native Addon Binding Extraction from JS AST) in tracker #4330.

<!-- gh-comment-id:6207087610 --> @matbrgz commented on GitHub (Oct 16, 2026): Cross-referencing with sibling issue #3884 (`[Bug] Local require method creates a resolved native-addon binding`). Both issues pinpoint the AST extraction of native-addon bindings in `analyze_javascript_application` (handling method shadowing vs optional/conditional require branches). Consolidated under **Cluster M (Native Addon Binding Extraction from JS AST)** in tracker #4330.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

nativeRequire now accepts both CallExpression and OptionalCallExpression and uses the shared require-origin classifier. The CLI/stdio-MCP acceptance fixture contains ordinary and optional unshadowed require, expects one binding for each, checks resolved counts, and retains dependency edges while excluding local/shadowed loaders. This directly covers the reported optional-call omission without executing the inert addon.

Implementation: electronStaticAnalysisNative.ts, javascriptNativeBindingOwnership.test.ts.

The relevant regression checks in javascriptNativeBindingOwnership.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380735893 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. nativeRequire now accepts both CallExpression and OptionalCallExpression and uses the shared require-origin classifier. The CLI/stdio-MCP acceptance fixture contains ordinary and optional unshadowed require, expects one binding for each, checks resolved counts, and retains dependency edges while excluding local/shadowed loaders. This directly covers the reported optional-call omission without executing the inert addon. Implementation: [electronStaticAnalysisNative.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/javascript/electronStaticAnalysisNative.ts#L297), [javascriptNativeBindingOwnership.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/acceptance/applications/javascriptNativeBindingOwnership.test.ts#L14). The relevant regression checks in [javascriptNativeBindingOwnership.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/acceptance/applications/javascriptNativeBindingOwnership.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1505
No description provided.