Skip to content

feat: fall back to other arities in go-to-definition when exact arity has no result#728

Open
mvanhorn wants to merge 1 commit into
expert-lsp:mainfrom
mvanhorn:feat/725-feat-fall-back-to-other-arities-in-go-to
Open

feat: fall back to other arities in go-to-definition when exact arity has no result#728
mvanhorn wants to merge 1 commit into
expert-lsp:mainfrom
mvanhorn:feat/725-feat-fall-back-to-other-arities-in-go-to

Conversation

@mvanhorn

Copy link
Copy Markdown

Summary

Go-to-definition on a function call now falls back to other arities of the same module.function when no definition exists for the exact arity. The exact-arity match still wins whenever it is present, so existing behavior is unchanged.

Why this matters

As reported in #725, refactoring a function's arity breaks go-to-definition from call sites whose arity no longer matches any definition: the lookup returns nothing even though the function clearly still exists under a different arity. Jumping to the (now differently-aritied) definition is exactly what you want while you are mid-refactor. The fallback only triggers on the zero-result case and returns the alternate-arity definitions ordered lowest-to-highest arity, as the reporter suggested.

The fallback reuses the same delegate-expansion and de-duplication pipeline as the exact-arity path, so defdelegate targets are resolved and multi-clause / default-argument functions are not reported multiple times.

Testing

Added tests in apps/expert/test/engine/code_intelligence/definition_test.exs:

  • a wrong-arity call (MyDefinition.greet("a", "b")) resolves to the existing greet/1 definition via the fallback,
  • an exact-arity call (MyDefinition.greet("World")) still resolves to greet/1 directly (behavior preserved).

Fixes #725

@doorgan doorgan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a refactor that seems unnecessary to me, but otherwise this looks good and works, thank you!

end
end)
|> Stream.uniq_by(& &1.subject)
|> expand_definitions()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this refactor is necessary

@katafrakt katafrakt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit concerned if this would not create inconsistent behaviour. This fixes cases when we can find the fallback in the search index, but I don't think it would works for cases when we fall back to ElixirSense resolution. Perhaps this is okay, as we are doing "best effort", but I'm not sure if it would not create false expectations.

Comment on lines +196 to +215
test "prefers exact arity definitions when present", %{
project: project,
uri: referenced_uri
} do
subject_module = ~q[
defmodule UsesRemoteFunction do
alias MyDefinition

def uses_greet() do
MyDefinition.gree|t("World")
end
end
]

assert {:ok, ^referenced_uri, definition_line} =
definition(project, subject_module, referenced_uri)

assert definition_line == ~S[ def «greet(name)» do]
end
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this test does what it says. It does not prefer exact arity, it just uses the only definition here (as there is only one and it's greet/1.

@doorgan

doorgan commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

@katafrakt feel free to override my approvals and request changes. I got used to the tracers indexer being more complete that I had missed there is no ElixirSense fallback implementation in this PR.

@mvanhorn

Copy link
Copy Markdown
Author

@katafrakt good catch — you're right that this only wires the arity fallback into the search-index path in definition.ex. The ElixirSense resolution path doesn't get the same fallback, so go-to-definition on a refactored arity recovers when the symbol resolves through the index but not when it falls through to ElixirSense.

My read is that this is acceptable as best-effort for #725 (the index path is the common case, and the fallback only triggers on the zero-result case so existing behavior is unchanged), but I share your concern about uneven expectations. Happy to go either way:

  • keep this PR scoped to the index path and open a follow-up to mirror the fallback in the ElixirSense resolution path, or
  • extend it here if you'd prefer both paths land together.

Let me know which you'd prefer and I'll adjust.

@katafrakt

Copy link
Copy Markdown
Member

TBH I'm not even sure it's possible to transfer this behaviour to the ElixirSense fallback. Can you verify that before we make a call here?

@mvanhorn

mvanhorn commented Jul 5, 2026

Copy link
Copy Markdown
Author

Verified, and you were right to be skeptical - it can't transfer cleanly. The fallback calls ElixirSense.definition(code, line, column), and at the version in our mix.lock (fb9ee0e) the options only carry parsed :metadata. Arity is computed internally in Providers.Definition.Locator via Metadata.get_call_arity(...) || :any and passed straight into the location lookup - there's no call-site arity override, and the API returns a single %Location{} or nil rather than same-name candidates. Retrying the call can't change the internally parsed arity.

Making it work would need an upstream ElixirSense API that accepts a candidate-arity override (or returns all same-name arity matches). So I'd propose we keep this PR index-path-only as best effort for #725, and I can file an upstream issue with ElixirSense if you think it's worth pursuing.

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.

Navigate to source should disconsider arity

3 participants