Skip to content

bind names in JS/TS destructuring patterns (silent slice under-approximation) - #115

Merged
sshaaf merged 1 commit into
sshaaf:mainfrom
samiujan:upstream/fix-destructuring-defs
Oct 14, 2026
Merged

sshaaf merged 1 commit into
sshaaf:mainfrom
samiujan:upstream/fix-destructuring-defs

Conversation

@samiujan

Copy link
Copy Markdown
Contributor

Fixes a silent soundness bug in rgctl-analysis def-use. Based on main (8f09655).

collect_pattern_defs did not recognise the JS/TS destructuring pattern node kinds, so a destructured name was never recorded as defined. rgctl slice therefore under-approximated silently — a well-formed slice with a plausible reduction_percent, missing the destructuring statement, with nothing indicating a dependence had been dropped.

export function pick(src: { a: number; b: number }) {
  const { a, b } = src;   // line 2 — binds a and b
  const total = a + b;    // line 3
  return total;
}

slice src/d.ts --line 3 --variable total --function pick
  before: { "lines": [3], "edges": [] }
  after:  { "lines": [2, 3], "edges": [...] }

Array destructuring failed the same way; a plain const base = 10; control was already correct, which isolates the gap to destructuring.

Cause: an unrecognised pattern reached the catch-all arm, which delegates to collect_def_use_node, whose own final fallback pushes children with is_def_target = false — so destructured names were walked as uses. shorthand_property_identifier_pattern was not in the shared-identifier arm either.

Fix: add object_pattern/array_pattern (recurse), pair_pattern (recurse into value only — key is a property name), object_assignment_pattern/assignment_pattern (recurse into left only — right is a default that is read), and shorthand_property_identifier_pattern (bind its own text). Widen is_binding_pattern, which guards the routing arm.

Not confined to slice: reaching definitions, the PDG, taint and wl_hash.rs all inherit def-use.

Verified: 6 new regression tests (shorthand, array, renaming, nested, rest, default) plus the full rgctl-analysis suite — 365 passed, 0 failed. fmt and clippy add no new findings. End-to-end confirmed on the fixture above.

Fixes #113. (Opening on sshaaf/rgctl, where #113 resolves to the reported issue, so the keyword
links and auto-closes it. This branch is also staged in the fork as samiujan#2, where the same
keyword would point at a fork-local issue that does not exist.)

`collect_pattern_defs` did not recognise the destructuring pattern node kinds, so a
destructured name was never recorded as *defined*. The visible effect is that
`rgctl slice` silently under-approximated: it returned a well-formed slice with a
plausible `reduction_percent` that was missing the destructuring statement, and
nothing indicated a dependence had been dropped.

    export function pick(src: { a: number; b: number }) {
      const { a, b } = src;   // line 2 — binds a and b
      const total = a + b;    // line 3
      return total;
    }

    slice src/d.ts --line 3 --variable total --function pick
      before: { "lines": [3], "edges": [] }          # line 2 missing
      after:  { "lines": [2, 3], "edges": [...] }    # dependence present

Array destructuring failed the same way. A plain `const base = 10;` control was
already correct, which isolates the gap to destructuring rather than to def-use in
general.

Cause: an unrecognised pattern reached the catch-all arm, which delegates to
`collect_def_use_node`, whose own final fallback pushes children with
`is_def_target = false` — so the destructured names were walked as *uses*.
`shorthand_property_identifier_pattern` was not in the shared-identifier arm
either, so it was not picked up as anything at all.

Add `object_pattern` and `array_pattern` (recurse), `pair_pattern` (recurse into
`value` only — the `key` is a property name, and binding it would invent a variable
that does not exist in the source), `object_assignment_pattern` and
`assignment_pattern` (recurse into `left` only — `right` is a default expression
that is read, so descending into it would invert the dependence), and
`shorthand_property_identifier_pattern` (bind its own text; it is a leaf whose kind
is not `identifier`). Widen `is_binding_pattern` to match, since it guards the
routing arm.

This is not confined to `slice`: reaching definitions, the PDG, taint and the clone
fragment hasher (`wl_hash.rs` consumes `defined_vars`) all inherit def-use.

Regression tests cover shorthand, array, renaming, nested, rest and default
patterns — the last two asserting the things that must *not* be bound. Also
verified end to end: the three fixtures above now return `lines: [2, 3]`.

Not addressed: a default expression (`{ a = fallback }`) is read, and this
function has no way to record a use. Recording it needs a `used` set threaded
through, which is a larger change; leaving it unrecorded is better than recording
it as a definition.

Signed-off-by: Sami Jan <sami.jan@gmail.com>
samiujan added a commit to samiujan/rgctl that referenced this pull request Oct 14, 2026
…monstrate

Two rows were out of date after the rebase onto main:

  - `dashboard/dist` regen was marked **blocked**, "source fixes are not in the
    embedded bundle until dist is rebuilt". That landed in 8f09655 as
    sshaaf#110. The row now says done, and says why it mattered: `dist` is
    compile-time embedded, so until it is rebuilt the source fixes are invisible in
    a built binary while every test passes.
  - Change 7 was blocked on two `rgctl slice` defects. Both are now fixed and under
    review (issues sshaaf#113/sshaaf#114, PRs sshaaf#115/sshaaf#116), so the row records what actually
    gates it.

Also adds open question 4, because the more interesting problem is a gap rather than
a stale value: the maintainer's parking comment on sshaaf#61 names the code browser as the
missing piece, and no change in this sequence is the code browser. Change 1 is the
shell, changes 2-6 are rooms, and none renders source. The prototype does not cover
for it either — the Code lens shows a count of source lines, not source, and Spine
renders an empty state. The one requirement singled out is the one this design
cannot demonstrate.

Recorded as an open question, not invented as a change number: folding source
rendering into change 1 versus giving it its own change ahead of the rooms is a
scoping decision, not a correction.

Signed-off-by: Sami Jan <sami.jan@gmail.com>
@sshaaf
sshaaf merged commit 5fedf5c into sshaaf:main Oct 14, 2026
2 checks passed
samiujan added a commit to samiujan/rgctl that referenced this pull request Oct 14, 2026
…monstrate

Two rows were out of date after the rebase onto main:

  - `dashboard/dist` regen was marked **blocked**, "source fixes are not in the
    embedded bundle until dist is rebuilt". That landed in 8f09655 as
    sshaaf#110. The row now says done, and says why it mattered: `dist` is
    compile-time embedded, so until it is rebuilt the source fixes are invisible in
    a built binary while every test passes.
  - Change 7 was blocked on two `rgctl slice` defects. Both are now fixed and under
    review (issues sshaaf#113/sshaaf#114, PRs sshaaf#115/sshaaf#116), so the row records what actually
    gates it.

Also adds open question 4, because the more interesting problem is a gap rather than
a stale value: the maintainer's parking comment on sshaaf#61 names the code browser as the
missing piece, and no change in this sequence is the code browser. Change 1 is the
shell, changes 2-6 are rooms, and none renders source. The prototype does not cover
for it either — the Code lens shows a count of source lines, not source, and Spine
renders an empty state. The one requirement singled out is the one this design
cannot demonstrate.

Recorded as an open question, not invented as a change number: folding source
rendering into change 1 versus giving it its own change ahead of the rooms is a
scoping decision, not a correction.

Signed-off-by: Sami Jan <sami.jan@gmail.com>
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.

[BUG] rgctl slice silently under-approximates on JS/TS destructuring patterns

2 participants