Skip to content

fix(flow): track available function signatures through assignments - #1356

Merged
CppCXY merged 1 commit into
EmmyLuaLs:mainfrom
lewis6991:refactor/callable-flow-resolution
Nov 4, 2026
Merged

CppCXY merged 1 commit into
EmmyLuaLs:mainfrom
lewis6991:refactor/callable-flow-resolution

Conversation

@lewis6991

Copy link
Copy Markdown
Collaborator

Signature help can show old overloads after a function is replaced by an assignment or a cast. This change keeps only the original signatures that can still apply at the call.

Two functions can have the same main signature and different overloads. The analyzer must track which function is used, even when its type stays the same:

---@param value number
---@overload fun(value: string): string
local function accept(value) return value end

---@param value number
local function replacement(value) return value end

accept = (accept) -- Keep the string overload.
accept = replacement -- Remove the string overload.

Reuse the existing code that follows assignments, casts and branches. Add a mode that keeps only the original signatures that still apply. Self-assignment keeps them. Other assignments and replacement casts remove them. Removal casts remove only matching signatures. Combine signatures from branches that can run.

The shared flow code needs these changes:

  • Use one work stack for pending assignments, casts and branches. Long branch chains can then reuse shared earlier results without deeply nested Rust calls. Keep the existing assignment type checks in one helper.
  • Save intermediate results so repeated calls can reuse earlier work. Keep later casts out of earlier saved results. Keep results separate for each mode, original function and full signature list, including generic type arguments.
  • Check current, fully known return types before using a function call to rule out a branch. The function must be local, have no replacement assignments and have no declared overloads. A generic return that can still be false or nil cannot provide this proof. Save the check of the function's assignments so each condition does not scan them again.

Keep the existing generic type information and the main signature from each branch where the original function still applies. Normal type inference keeps its current function type information.

This shared support also lets later annotation and pcall checks use the same assignment and branch rules. A separate signature tracker would need another copy of those rules and another cache. Annotation priority, argument checks through pcall, and wider changes to function-call inference remain separate work.

Signature help must show the function signatures available at each call.
These describe the parameters and return values. The function definition
provides the main signature. An overload annotation adds another one.
Assignments, casts and branches can change which signatures apply.

Two functions can have the same main signature and different overloads.
The analyzer must track which function is used, even when its type stays
the same:

    ---@PARAM value number
    ---@overload fun(value: string): string
    local function accept(value)
        return value
    end

    ---@PARAM value number
    local function replacement(value)
        return value
    end

    accept = (accept) -- keep the string overload

    accept = replacement -- remove the string overload

The existing code reads the current function type and then adds the
signatures from the original declaration. This can show an old overload
after a replacement or a cast.

Reuse the code that already follows assignments, casts and branches. Add
a mode that keeps only the original signatures that still apply.
Assigning the function to itself keeps its signatures. Other assignments
remove them. Casts that replace a type remove them. Casts that remove a
type remove only matching signatures. Combine results from branches that
can run.

Use this mode to collect the available function signatures. Later
changes can use the same support to check function annotations and
arguments passed through pcall. A separate signature tracker would need
another copy of the assignment, loop and branch rules, and another
cache.

The changes to the shared code are needed for these reasons:

- Use one work stack for pending assignments, casts and branches. Keep
  the existing assignment type checks in one helper as the stack
  changes.
- Save results at each step, instead of only where a check starts.
  Repeated calls can then reuse earlier work. Keep later casts out of
  earlier saved results.
- Check shared earlier code before checking each branch. Long chains of
  branches can then reuse saved results without deeply nested Rust
  calls.
- Identify saved results by the check mode, original function and full
  signature list. Results for different generic type arguments, or lists
  with and without the main signature, must stay separate. Keep the
  existing rules for loops and unfinished checks.
- Check the current return types before using a function call to rule
  out a branch. The types must be fully known. A generic return can
  still be false or nil until the call arguments set its type. Such a
  return cannot rule out a branch. The function must be local and have
  no replacement assignments or declared overloads. Its original return
  annotation can describe an earlier function value. Save the result of
  checking its assignments so each condition does not scan them all
  again.

Use signatures from the current value for replacement functions and
casts that add types. Keep the original main signature if it can still
be used on another branch. Keep the existing generic type information.

Lua tests check signature-help text, inferred types and the amount of
work for repeated calls and long branch chains. Tests and their support
add 637 lines. The 425 net new lines in flow.rs support the new mode,
the shared work stack, saved results and the checks on function return
types.

Normal type inference keeps its current function type information. Rules
for which annotation takes priority, argument checks through pcall, and
wider changes to function-call inference remain separate work.
@CppCXY
CppCXY merged commit de89376 into EmmyLuaLs:main Nov 4, 2026
16 of 17 checks passed
@lewis6991
lewis6991 deleted the refactor/callable-flow-resolution branch November 4, 2026 07:57
mareklangiewicz added a commit to mareklangiewicz/emmylua-analyzer-rust that referenced this pull request Nov 4, 2026
Upstream replaced TraceOptions with DeclTracePurpose. The fork's capture
read moves onto it as `ReadValue { capture }`. A field, not a new variant,
so every upstream ReadValue use had to be looked at: a variant would have
left `matches!(purpose, ReadValue)` false for captured reads.

Fork behaviour kept: unknown_assign_keeps_kind, a nil base for an
uninitialized local (now decl_position_type) and the memo for guards
collected before a node.

Two changes the merge needed, both from upstream's new tests:

- live_decl_overloads appended a local's overloads whatever casts ran
  before the read. overload_candidates_for_decl now goes through
  original_decl_signatures_at, which replaces the fork's check for any
  earlier assignment, so an assignment on one branch no longer drops
  overloads on the other.
- Incoming casts now apply to the memoized state as guards do. They used
  to skip the memo, and 64 branches with casts ran past the 5 s limit in
  test_deep_branch_casts_merge_surviving_overloads. Correlated narrowings
  still take the uncached walk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NxUUNMvTbsNag6WkLaLu7
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.

2 participants