Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track uses and requires #756

Merged
merged 1 commit into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/common/lib/lexical/ast/analysis.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ defmodule Lexical.Ast.Analysis do

alias Lexical.Ast.Analysis.Alias
alias Lexical.Ast.Analysis.Import
alias Lexical.Ast.Analysis.Require
alias Lexical.Ast.Analysis.Scope
alias Lexical.Ast.Analysis.State
alias Lexical.Ast.Analysis.Use
alias Lexical.Document
alias Lexical.Document.Position
alias Lexical.Document.Range
Expand Down Expand Up @@ -365,6 +367,40 @@ defmodule Lexical.Ast.Analysis do
State.push_import(state, Import.new(state.document, quoted, module))
end

# require of a module using as
scohen marked this conversation as resolved.
Show resolved Hide resolved
defp analyze_node(
{:require, _meta, [{:__aliases__, _, module}, options]} = quoted,
state
) do
{_, as_module} =
Macro.prewalk(options, nil, fn
{:__aliases__, _, as} = ast, nil ->
{ast, as}

ast, acc ->
{ast, acc}
end)

State.push_require(state, Require.new(state.document, quoted, module, as_module))
scohen marked this conversation as resolved.
Show resolved Hide resolved
end

# require a module
scohen marked this conversation as resolved.
Show resolved Hide resolved
defp analyze_node(
{:require, _meta, [{:__aliases__, _, module}]} = quoted,
state
) do
State.push_require(state, Require.new(state.document, quoted, module))
end

# use statement

scohen marked this conversation as resolved.
Show resolved Hide resolved
defp analyze_node(
{:use, _meta, [{:__aliases__, _, module} | opts]} = use,
state
) do
State.push_use(state, Use.new(state.document, use, module, opts))
end

# stab clauses: ->
defp analyze_node({clause, _, _} = quoted, state) when clause in @clauses do
maybe_push_scope_for(state, quoted)
Expand Down
10 changes: 10 additions & 0 deletions apps/common/lib/lexical/ast/analysis/require.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Lexical.Ast.Analysis.Require do
alias Lexical.Ast
alias Lexical.Document
defstruct [:module, :as, :range]

def new(%Document{} = document, ast, module, as \\ nil) when is_list(module) do
range = Ast.Range.get(ast, document)
%__MODULE__{module: module, as: as || module, range: range}
end
end
16 changes: 14 additions & 2 deletions apps/common/lib/lexical/ast/analysis/scope.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ defmodule Lexical.Ast.Analysis.Scope do
:range,
module: [],
aliases: [],
imports: []
imports: [],
requires: [],
uses: []
]

@type import_mfa :: {module(), atom(), non_neg_integer()}
Expand All @@ -23,12 +25,22 @@ defmodule Lexical.Ast.Analysis.Scope do
}

def new(%__MODULE__{} = parent_scope, id, %Range{} = range, module \\ []) do
uses =
if module == parent_scope.module do
# if we're still in the same module, we have the same uses
zachallaun marked this conversation as resolved.
Show resolved Hide resolved
parent_scope.uses
else
[]
end

%__MODULE__{
id: id,
aliases: parent_scope.aliases,
imports: parent_scope.imports,
requires: parent_scope.requires,
module: module,
range: range
range: range,
uses: uses
}
end

Expand Down
14 changes: 14 additions & 0 deletions apps/common/lib/lexical/ast/analysis/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ defmodule Lexical.Ast.Analysis.State do
alias Lexical.Ast.Analysis
alias Lexical.Ast.Analysis.Alias
alias Lexical.Ast.Analysis.Import
alias Lexical.Ast.Analysis.Require
alias Lexical.Ast.Analysis.Scope
alias Lexical.Ast.Analysis.Use
alias Lexical.Document
alias Lexical.Document.Position
alias Lexical.Document.Range
Expand Down Expand Up @@ -88,6 +90,18 @@ defmodule Lexical.Ast.Analysis.State do
end)
end

def push_require(%__MODULE__{} = state, %Require{} = require) do
update_current_scope(state, fn %Scope{} = scope ->
Map.update!(scope, :requires, &[require | &1])
end)
end

def push_use(%__MODULE__{} = state, %Use{} = use) do
update_current_scope(state, fn %Scope{} = scope ->
Map.update!(scope, :uses, &[use | &1])
end)
end

defp update_current_scope(%__MODULE__{} = state, fun) do
update_in(state, [Access.key(:scopes), Access.at!(0)], fn %Scope{} = scope ->
fun.(scope)
Expand Down
10 changes: 10 additions & 0 deletions apps/common/lib/lexical/ast/analysis/use.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Lexical.Ast.Analysis.Use do
alias Lexical.Ast
alias Lexical.Document
defstruct [:module, :range, :opts]

def new(%Document{} = document, ast, module, opts) do
range = Ast.Range.get(ast, document)
%__MODULE__{range: range, module: module, opts: opts}
end
end
28 changes: 28 additions & 0 deletions apps/remote_control/lib/lexical/remote_control/analyzer.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
defmodule Lexical.RemoteControl.Analyzer do
alias Lexical.Ast
alias Lexical.Ast.Analysis
alias Lexical.Ast.Analysis.Require
alias Lexical.Ast.Analysis.Use
alias Lexical.Document.Position
alias Lexical.RemoteControl.Analyzer.Aliases
alias Lexical.RemoteControl.Analyzer.Imports
alias Lexical.RemoteControl.Analyzer.Requires
alias Lexical.RemoteControl.Analyzer.Uses

require Logger

defdelegate aliases_at(analysis, position), to: Aliases, as: :at
defdelegate imports_at(analysis, position), to: Imports, as: :at

@spec requires_at(Analysis.t(), Position.t()) :: [module()]
def requires_at(%Analysis{} = analysis, %Position{} = position) do
analysis
|> Requires.at(position)
|> Enum.reduce([], fn %Require{} = require, acc ->
case expand_alias(require.module, analysis, position) do
{:ok, expanded} -> [expanded | acc]
_ -> [Module.concat(require.as) | acc]
end
end)
end

@spec uses_at(Analysis.t(), Position.t()) :: [module()]
def uses_at(%Analysis{} = analysis, %Position{} = position) do
analysis
|> Uses.at(position)
|> Enum.reduce([], fn %Use{} = use, acc ->
case expand_alias(use.module, analysis, position) do
{:ok, expanded} -> [expanded | acc]
_ -> [Module.concat(use.module) | acc]
end
end)
end
scohen marked this conversation as resolved.
Show resolved Hide resolved

def resolve_local_call(%Analysis{} = analysis, %Position{} = position, function_name, arity) do
maybe_imported_mfa =
analysis
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule Lexical.RemoteControl.Analyzer.Requires do
alias Lexical.Ast.Analysis
alias Lexical.Ast.Analysis.Require
alias Lexical.Ast.Analysis.Scope
alias Lexical.Document.Position

def at(%Analysis{} = analysis, %Position{} = position) do
case Analysis.scopes_at(analysis, position) do
[%Scope{} = scope | _] ->
scope.requires
|> Enum.filter(fn %Require{} = require ->
require_end = require.range.end

if require_end.line == position.line do
require_end.character <= position.character
else
require_end.line < position.line
end
zachallaun marked this conversation as resolved.
Show resolved Hide resolved
end)
|> Enum.uniq_by(& &1.as)
zachallaun marked this conversation as resolved.
Show resolved Hide resolved

_ ->
[]
end
end
end
23 changes: 23 additions & 0 deletions apps/remote_control/lib/lexical/remote_control/analyzer/uses.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Lexical.RemoteControl.Analyzer.Uses do
alias Lexical.Ast.Analysis
alias Lexical.Ast.Analysis.Scope
alias Lexical.Document.Position

def at(%Analysis{} = analysis, %Position{} = position) do
case Analysis.scopes_at(analysis, position) do
[%Scope{} = scope | _] ->
Enum.filter(scope.uses, fn use ->
use_end = use.range.end

if position.line == use_end.line do
position.character >= use_end.character
else
position.line > use_end.line
end
end)

_ ->
[]
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
defmodule Lexical.RemoteControl.Analyzer.RequiresTest do
alias Lexical.Ast
alias Lexical.RemoteControl.Analyzer

import Lexical.Test.CursorSupport
import Lexical.Test.CodeSigil

use ExUnit.Case

def requires_at_cursor(text) do
{position, document} = pop_cursor(text, as: :document)

document
|> Ast.analyze()
|> Analyzer.requires_at(position)
end

describe "requires at the top level" do
test "are not present before the require statement" do
requires = requires_at_cursor("|require OtherModule")

assert Enum.empty?(requires)
end

test "work for a single require" do
requires = requires_at_cursor("require OtherModule|")

assert requires == [OtherModule]
end

test "handles aliased modules" do
requires =
~q[
alias Other.MyModule
require MyModule|
]
|> requires_at_cursor()

assert requires == [Other.MyModule]
end

test "handles as" do
requires =
~q[
require Other.Module, as: ReqMod
|
]
|> requires_at_cursor()

assert requires == [Other.Module]
end

test "work for a multiple require" do
requires =
~q[
require First
require Second
require Third
|
]
|> requires_at_cursor()

assert requires == [First, Second, Third]
end
end

describe "in modules" do
test "begin after the require statement" do
requires =
~q[
defmodule Outer do
require Required|
end
]
|> requires_at_cursor()

assert requires == [Required]
end

test "ends after the module" do
requires =
~q[
defmodule Outer do
require Required
end|
]
|> requires_at_cursor()

assert requires == []
end

test "carries over to nested modules" do
requires =
~q[
defmodule Outer do
require Required
defmodule Inner do
|
end
end
]
|> requires_at_cursor()

assert requires == [Required]
end
end
end
Loading
Loading