Skip to content

Regression (#1759): store-action bindings treated as local shadows; main fails its own object-literal-methods test #1762

Closed as not planned

Description

@bompus

Summary

#1759 ("a receiver-less JS/TS call never binds to a method") makes isLocallyBoundJsName treat a Zustand-style store binding as a local rebinding, so bare calls to store actions stop resolving. This regresses the repo's own test suite: main at cd4e65b fails one of its own tests.

Reproduction

Clean checkout of main at cd4e65b, npm ci, full build, then:

npx vitest run __tests__/object-literal-methods.test.ts
FAIL __tests__/object-literal-methods.test.ts > object-literal method resolution (end-to-end)
   > resolves callers of store actions across files (destructured + chained getState())

AssertionError: expected [] to include 'loginFlow'

__tests__/ui-steps-api.test.ts fails the same way (2 tests):

  • buildSteps > walks a screen through its handler, the bridge, the event, the store and the request
  • screen regions > a screen names every step's region: the screen body for its own code, inherited down the walk

Cause

isLocallyBoundJsName carves out require(...) / import(...) on the right-hand side, because an imported name is a node elsewhere rather than a shadow. A store binding is the same situation and is not carved out:

const { fetchUser } = useStore.getState();   // action lives in the store file
const setZipUri = useStore((s) => s.setZipUri);

Both name the store's own action, so the bare call that follows means that action. matchStoreAccessorChain already recognises the accessor shapes; isLocallyBoundJsName now discards those candidates before it runs.

Fix

Extend the existing carve-out to the two accessor shapes plus the selector read. PR follows.

The selector pattern is deliberately narrow — one named parameter and a plain member read of the same name — so a genuine local binding such as const now = options.now || (() => Date.now()) still shadows. That case is covered by bare-call-no-method, which stays green.

Verification

Full suite on the same machine, same build, pristine vs patched:

pristine cd4e65b with the fix
Test files 13 failed 7 failed
Unique failing tests 29 24

object-literal-methods and both ui-steps-api tests go green, and no test that passed before fails after. The remaining failures are pre-existing on this machine (Windows, native kernel not built) and byte-identical across both runs.

Activity

  1. bompus commented on Sep 8, 2026

    @bompus
    ContributorAuthor

    Closing as a duplicate of #1760, which diagnoses the same regression and covers the same two store shapes. I filed this without checking the open PR list first — apologies for the noise.

    Leaving the independent verification here in case a second platform is useful, since #1760's measurements are from wasm arm and these are Windows x64:

    Clean checkout of main at cd4e65b, full build, full suite — pristine vs a patch of the same shape:

    pristine cd4e65b patched
    Test files 13 failed 7 failed
    Unique failing tests 29 24

    The three that go green are exactly the ones #1760 names — object-literal-methods.test.ts > resolves callers of store actions across files (destructured + chained getState()) (expected [] to include 'loginFlow') and the two in ui-steps-api.test.ts. No test that passed before failed after. The remaining 24 are pre-existing on this machine (native kernel not built) and identical across both runs.

    One difference worth flagging for #1760: I kept const now = options.now || (() => Date.now()) shadowing, by requiring the selector shape to be a single named parameter plus a member read ((?!\s*\()), so bare-call-no-method stayed green unmodified. #1760 instead reclassifies that case as a re-binding and drops it from the fixture. That is a deliberate call in #1760 and the reasoning there is sound — noting it only so the choice is explicit.

  2. danusha2345 commented on Sep 8, 2026

    @danusha2345
    Contributor

    Thanks for the Windows x64 confirmation. One correction on the flagged difference: that was #1760's first commit. Its second commit (a29d112) went the same way you did — const now = options.now || … is a local binding again and the bare-call-no-method fixture is back unmodified. The exemption now covers only destructuring and a selector arrow that reads the same member off its own parameter ((s) => s.name), so () => Date.now() still shadows. Measured on a Kotlin + JS + Go app: the wide version put 24 now() calls onto a Kotlin private val now; the narrow one puts them nowhere.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions