Skip to content

Advanced alias scoping: block-level scope, and aliases below the reference #95

Open

Description

@JesseHerrick

Background

PR #93 made nested-module alias scoping match Elixir on all three paths:
alias chains (c149bad), the open-buffer scope map (4bfe625), and
use-injected aliases (12f3103). Two smaller deviations from Elixir's
lexical rules are still open. Both are narrow, and neither is urgent.

Gap A — an alias in a function body escapes into the whole module

Elixir scopes an alias to the enclosing block. An alias written in one
def body is not in scope in the next one.

defmodule M do
  def a do
    alias My.App.Repo
    Repo.all()
  end

  def b do
    Repo.all()   # Elixir: undefined alias. Both paths: My.App.Repo
  end
end

Observed: ExtractAliasesInScope at the line in def b returns
{"Repo": "My.App.Repo"}, and the index records the line-7 call as
My.App.Repo.

The two paths agree with each other, so nothing half-applies. The cost is
a wrong hover, a wrong jump, and a false reference on the second call.
The same applies to any block: fn, case clauses, if branches.

Gap B — the buffer accepts an alias written below the reference

defmodule M do
  def a, do: Repo.all()   # buffer: My.App.Repo. Index: bare Repo

  alias My.App.Repo
end

Here the paths disagree. The buffer filters by scope only, so it returns
an alias from anywhere in the module. The index resolves in a forward
pass, so the call stays bare. Hover resolves the call, references does
not find it, and a rename can apply to one and not the other — the same
class of half-applied edit that 12f3103 fixed.

Note the leniency may be deliberate: mid-edit, a person can write the
call before the alias. If we keep it, the index should match it, not the
other way round.

Suggested approach

The block machinery already exists. lexicalScopeAt (server.go:2696)
builds a stack of open block tokens and scopeContains (:2736) tests
prefix containment; the injected-alias path uses both.
extractAliasesFromTokens (internal/lsp/elixir.go) instead keys frames
on modules only, through parentScopes.

Widen those frames from module blocks to all blocks. Keeping the flat
alias list and recording the block stack per entry (as parentScopes
does now for modules) avoids a map copy at every do and fn. Then the
final filter becomes the containment test that Gap A needs, and the line
test that 4bfe625 added already covers Gap B.

Mirror whatever we choose in parser_tokenized.go, which saves and
restores the alias map per module frame today.

Acceptance criteria

  • An alias in a def body is not in scope in a later def.
  • The same for fn, case, if and other blocks.
  • The buffer path and the index agree on an alias written below the
    reference, whichever behavior we pick.
  • No measurable slowdown in ExtractAliasesInScope. Reference: the
    nested-module change cost 0 time and +11 allocs on a file with 30
    nested modules and 170 aliases.
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

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions