Fix/typespec and alias chain resolution - #93
Open
JesseHerrick wants to merge 5 commits into
Open
Conversation
Typespec mentions of a module (`Ecto.Schema.schema()` in an `@spec`) were indexed as calls, so they showed up in find-references for the macro of that name and drove go-to-definition and hover. They now carry their own kind: a function lookup drops them, a type lookup keeps only them, and inside a typespec definition and hover prefer the type while outside one they prefer the function. Alongside that: - Functions win over same-named types outside a typespec (and types win inside one), instead of whichever came first in the file - Private namesakes are dropped as definition targets when the call site sits outside the defining module and a public namesake exists - Alias lines whose leading segment is itself an alias (`alias Accounts.Users` after `alias SharedLib.Accounts`) resolve to the canonical module, in both the index parser and the open-document parser - `require Mod, as: Name` inside a `quote do` block is kept, so modules that `use` the injector can resolve the short name IndexVersion goes to 15 for the new reference kind and the alias resolution change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three independent bugs, all found while a module rename left work behind.
A predicate name inside a string interpolation ends in `?`, which also
opens a char literal in Elixir. The tokenizer read the `?` of
`#{dry_run?}` as a char literal, swallowing the interpolation's closing
brace and scanning the rest of the file as string content — so every
definition and reference below that line was silently absent from the
index.
A module whose `__using__` declares `alias MyApp.Repo` puts that short
name in its users' scope, so those files write `Repo.insert(...)` with
no alias line. The index holds those sites under the bare `Repo`, where
a lookup for `MyApp.Repo` cannot find them, and a rename left them
pointing at a module that no longer existed. The injecting module is
now found from the renamed module's own references — the `alias` inside
a `__using__` body is itself an indexed reference — so the discovery
costs one small query when nothing injects the module, rather than a
scan of every `__using__` in the project. On a 1,500-file project the
rename covers 1,368 sites instead of 694, and references to Plausible's
Repo went from 249 to 935 with no measurable change to any other
lookup (9ms vs 3ms for that module; identical elsewhere).
Renaming the argument on the right of a `with` clause renamed the
binding introduced by that same clause's pattern, reaching into the
later pin and the do block. A clause's expression is evaluated before
its pattern binds, so it names what the previous clause bound.
IndexVersion goes to 16 for the tokenizer fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4c8b400. Configure here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Five fixes across the parser, the index-side tokenizer, and the LSP's rename/references paths. Two of them lost work silently: a module rename that left call sites pointing at a module that no longer existed, and a tokenizer misread that dropped everything below a line from the index.
IndexVersiongoes to 16 (new reference kind, alias resolution change, tokenizer fix). Existing indexes rebuild on next start.Rename missed call sites reached through an alias injected by
useA module whose
__using__block declaresalias MyApp.Repoputs that short name in the scope of every module that uses it, so those files writeRepo.insert(...)with no alias line of their own. The index holds such a site under the bareRepo, which a lookup forMyApp.Repocannot see — so renaming the module rewrote theuselines and left every one of those calls behind.The injecting module is found from the renamed module's own references: the
aliasline inside a__using__body is itself an indexed reference. So discovery is a couple of small indexed queries rather than statting every__using__in the project, and a rename that injects nothing pays essentially nothing. Aliases injected through anExUnit.CaseTemplate(using dorather thandefmacro __using__) are included — that is how test files reach theirs, and it was the difference between 669 and 935 references below.Go-to-references now finds these call sites for a function too. For a module the existing convention is kept: it answers with the alias/import/use sites that name the module, not with every call made through it.
Measured on a 1,484-file / 108k-reference project:
Repo.left in codeThe remaining mentions of the old name after the rename are comments, docstrings and test names.
Everything after
#{flag?}fell out of the indexA predicate name inside a string interpolation ends in
?, which also opens a char literal in Elixir (?a). The tokenizer read the?ofthe second way, so it swallowed the interpolation's closing brace and scanned the whole rest of the file as string content. Every definition and reference below that line was silently missing from the index. One real file was indexed to line 86 of 240+, which is why its
Repo.insert!calls survived even the fixed rename.A
?straight after a name now ends that name; a char literal in operand position still tokenizes as one.Renaming a
withclause's argument renamed the wrong bindingA clause's expression is evaluated before that clause's own pattern binds, so the
siteinsideauthorize(...)names binding A. Renaming it reached forward into the second pattern, the later^sitepin and thedoblock — all of which belong to B.The occurrences of a clause's binding now stop at the next pattern that binds the name anew. Two related cases fell out of the same fix: a pinned pattern (
{:ok, ^site} <-) binds nothing, so it and the body still follow the earlier binding; and a pin in the first clause names the outer binding, which previously returned no occurrences at all.Typespec mentions counted as calls
A module reference written in a typespec —
Ecto.Schema.schema()in@spec put_meta(Ecto.Schema.schema(), meta)— names a type but was indexed as a call, so it turned up in find-references for the macro of that name and drove go-to-definition and hover. Typespec references now carry their own kind: a function lookup drops them, a type lookup keeps only them, and inside a typespec definition and hover prefer the type while outside one they prefer the function.Alongside it: functions win over same-named types outside a typespec (
Ecto.Schemadeclares both@type schemaanddefmacro schema/2); private namesakes are no longer offered as definition targets when the call site sits outside the defining module and a public namesake exists.Aliases built on other aliases
alias SharedLib.Accountsfollowed byalias Accounts.Users,alias Accounts.{Tokens, Roles}orrequire Accounts.Macros, as: Mkept the short prefix instead of resolving to the canonical module, so definition, hover and references missed the target. The leading segment of an alias/require line is now expanded through the aliases already in scope, in both the index parser and the open-document parser. Arequire Mod, as: Nameinside aquote doblock is also kept, so modules thatusethe injector can resolve the short name.Note
Medium Risk
Large changes to indexing, rename, and reference resolution affect core LSP behavior project-wide;
IndexVersion17 forces a full rebuild, but extensive tests cover the new edge cases.Overview
This release (0.7.2,
IndexVersion17) tightens Elixir LSP indexing and navigation across the tokenizer, index parser, and server rename/references/definition/hover paths.Indexing & parsing: Predicate names ending in
?inside#{...}no longer break tokenization and drop the rest of the file. Module refs in@type/@spec/@callbackare indexed astypespec(notcall), with sharedScanTypespecEndboundaries. Alias/require lines resolve throughExpandAliasPrefix(chained aliases andrequire …, as:in__using__bodies).Navigation: Go-to-definition/hover pick function vs type from cursor context (
InTypespec), drop private namesakes for remote/imported calls when a public match exists, and references filter by typespec vs call. Same-file bare type uses merge with indexed refs; type parameters still use the variable scan.Rename & references: Module rename and find-references now include call sites that only see a module through a
use-injected alias (Repo.allwith no local alias), with lexical pairing ofusesites, dispatch targets (:controllervs:plain), and column-scoped edits;ExUnit.CaseTemplateinjectors are included viaLookupCaseTemplateModules.Rename (variables):
withclause occurrences respect evaluation order (RHS uses the previous binding; rebinding stops at the next pattern; pins handled correctly).Reviewed by Cursor Bugbot for commit 03aa65f. Bugbot is set up for automated code reviews on this repo. Configure here.