Skip to content

Fix/typespec and alias chain resolution - #93

Open
JesseHerrick wants to merge 5 commits into
mainfrom
fix/typespec-and-alias-chain-resolution
Open

Fix/typespec and alias chain resolution#93
JesseHerrick wants to merge 5 commits into
mainfrom
fix/typespec-and-alias-chain-resolution

Conversation

@JesseHerrick

@JesseHerrick JesseHerrick commented Sep 8, 2026

Copy link
Copy Markdown
Member

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.

IndexVersion goes 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 use

A module whose __using__ block declares alias MyApp.Repo puts that short name in the scope of every module that uses it, so those files write Repo.insert(...) with no alias line of their own. The index holds such a site under the bare Repo, which a lookup for MyApp.Repo cannot see — so renaming the module rewrote the use lines and left every one of those calls behind.

The injecting module is found from the renamed module's own references: the alias line 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 an ExUnit.CaseTemplate (using do rather than defmacro __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:

before after
rename sites 694 1,368
bare Repo. left in code 624 0
references to that module 249 935 (3ms → 9ms)
references to any other module or function identical count and timing

The remaining mentions of the old name after the rename are comments, docstrings and test names.

Everything after #{flag?} fell out of the index

A predicate name inside a string interpolation ends in ?, which also opens a char literal in Elixir (?a). The tokenizer read the ? of

IO.puts("Backfilling (DRY RUN: #{dry_run?})...")

the 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 with clause's argument renamed the wrong binding

with {:ok, site} <- normalize(site),      # binding A
     {:ok, site} <- authorize(site) do    # A on the right, B on the left

A clause's expression is evaluated before that clause's own pattern binds, so the site inside authorize(...) names binding A. Renaming it reached forward into the second pattern, the later ^site pin and the do block — 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.Schema declares both @type schema and defmacro 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.Accounts followed by alias Accounts.Users, alias Accounts.{Tokens, Roles} or require Accounts.Macros, as: M kept 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. A require Mod, as: Name inside a quote do block is also kept, so modules that use the injector can resolve the short name.


Note

Medium Risk
Large changes to indexing, rename, and reference resolution affect core LSP behavior project-wide; IndexVersion 17 forces a full rebuild, but extensive tests cover the new edge cases.

Overview
This release (0.7.2, IndexVersion 17) 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/@callback are indexed as typespec (not call), with shared ScanTypespecEnd boundaries. Alias/require lines resolve through ExpandAliasPrefix (chained aliases and require …, 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.all with no local alias), with lexical pairing of use sites, dispatch targets (:controller vs :plain), and column-scoped edits; ExUnit.CaseTemplate injectors are included via LookupCaseTemplateModules.

Rename (variables): with clause 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.

JesseHerrick and others added 2 commits September 8, 2026 01:12
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>
@JesseHerrick JesseHerrick self-assigned this Sep 8, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread internal/lsp/elixir.go

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread internal/lsp/server.go Outdated
Comment thread internal/lsp/server.go

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread internal/lsp/server.go Outdated
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.

1 participant