fix: support new versions of ecto's struct fields

fix: fixes for elixir_sense plugin
This commit is contained in:
Zach Daniel 2021-12-19 00:12:10 -05:00
parent 951eb256fc
commit 2986838a19
5 changed files with 577 additions and 49 deletions

View file

@ -358,12 +358,13 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do
Enum.flat_map(
extensions,
fn extension ->
if :erlang.function_exported(extension, :sections, 0) do
try do
Enum.filter(extension.sections(), fn section ->
Matcher.match?(to_string(section.name), hint)
end)
else
[]
rescue
_ ->
[]
end
end
)
@ -373,7 +374,7 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do
Enum.flat_map(
extensions,
fn extension ->
if :erlang.function_exported(extension, :sections, 0) do
try do
Enum.flat_map(extension.sections(), fn section ->
if section.name == first do
do_find_constructors(section, rest, hint, type)
@ -381,8 +382,9 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do
[]
end
end)
else
[]
rescue
_ ->
[]
end
end
)

View file

@ -32,7 +32,7 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do
end
custom =
for module <- module_store.by_behaviour[behaviour],
for module <- module_store.by_behaviour[behaviour] || [],
mod_str = inspect(module),
!String.starts_with?(mod_str, "Ash."),
Util.match_module?(mod_str, hint) do

View file

@ -407,20 +407,24 @@ defmodule Ash.Filter do
@doc "Whether or not a given template contains an actor reference"
def template_references_actor?({:_actor, _}), do: true
def template_references_actor?(filter) when is_list(filter) do
Enum.any?(filter, &template_references_actor?/1)
def template_references_actor?(%BooleanExpression{op: :and, left: left, right: right}) do
template_references_actor?(left) || template_references_actor?(right)
end
def template_references_actor?(filter) when is_map(filter) do
filter
|> Map.delete(:__struct__)
|> Enum.any?(fn {key, value} ->
template_references_actor?(key) || template_references_actor?(value)
end)
def template_references_actor?(%Not{expression: expression}) do
template_references_actor?(expression)
end
def template_references_actor?(tuple) when is_tuple(tuple) do
Enum.any?(Tuple.to_list(tuple), &template_references_actor?/1)
def template_references_actor?(%{left: left, right: right}) do
template_references_actor?(left) || template_references_actor?(right)
end
def template_references_actor?(%{arguments: args}) do
Enum.any?(args, &template_references_actor?/1)
end
def template_references_actor?(%Ash.Query.Call{args: args}) do
Enum.any?(args, &template_references_actor?/1)
end
def template_references_actor?(_), do: false

View file

@ -14,9 +14,19 @@ defmodule Ash.Schema do
@primary_key false
embedded_schema do
struct_fields_name =
if Module.get_attribute(__MODULE__, :struct_fields) do
:struct_fields
else
:ecto_struct_fields
end
for relationship <- Ash.Resource.Info.relationships(__MODULE__) do
@struct_fields {relationship.name,
%Ash.NotLoaded{type: :relationship, field: relationship.name}}
Module.put_attribute(
__MODULE__,
struct_fields_name,
{relationship.name, %Ash.NotLoaded{type: :relationship, field: relationship.name}}
)
end
for attribute <- Ash.Resource.Info.attributes(__MODULE__) do
@ -46,13 +56,11 @@ defmodule Ash.Schema do
field(aggregate.name, Ash.Type.ecto_type(type), virtual: true)
struct_fields = Keyword.delete(@struct_fields, aggregate.name)
Module.delete_attribute(__MODULE__, :struct_fields)
Module.register_attribute(__MODULE__, :struct_fields, accumulate: true)
Enum.each(struct_fields, &Module.put_attribute(__MODULE__, :struct_fields, &1))
@struct_fields {aggregate.name,
%Ash.NotLoaded{type: :aggregate, field: aggregate.name}}
Module.put_attribute(
__MODULE__,
struct_fields_name,
{aggregate.name, %Ash.NotLoaded{type: :aggregate, field: aggregate.name}}
)
end
for calculation <- Ash.Resource.Info.calculations(__MODULE__) do
@ -60,14 +68,17 @@ defmodule Ash.Schema do
field(calculation.name, Ash.Type.ecto_type(calculation.type), virtual: true)
struct_fields = Keyword.delete(@struct_fields, calculation.name)
Module.delete_attribute(__MODULE__, :struct_fields)
Module.register_attribute(__MODULE__, :struct_fields, accumulate: true)
Enum.each(struct_fields, &Module.put_attribute(__MODULE__, :struct_fields, &1))
@struct_fields {calculation.name,
%Ash.NotLoaded{type: :calculation, field: calculation.name}}
Module.put_attribute(
__MODULE__,
struct_fields_name,
{calculation.name, %Ash.NotLoaded{type: :calculation, field: calculation.name}}
)
end
struct_fields = Keyword.new(Module.get_attribute(__MODULE__, struct_fields_name))
Module.delete_attribute(__MODULE__, struct_fields_name)
Module.register_attribute(__MODULE__, struct_fields_name, accumulate: true)
Enum.each(struct_fields, &Module.put_attribute(__MODULE__, struct_fields_name, &1))
end
end
else
@ -77,9 +88,19 @@ defmodule Ash.Schema do
@primary_key false
schema Ash.DataLayer.source(__MODULE__) do
struct_fields_name =
if Module.get_attribute(__MODULE__, :struct_fields) do
:struct_fields
else
:ecto_struct_fields
end
for relationship <- Ash.Resource.Info.relationships(__MODULE__) do
@struct_fields {relationship.name,
%Ash.NotLoaded{type: :relationship, field: relationship.name}}
Module.put_attribute(
__MODULE__,
struct_fields_name,
{relationship.name, %Ash.NotLoaded{type: :relationship, field: relationship.name}}
)
end
for attribute <- Ash.Resource.Info.attributes(__MODULE__) do
@ -109,13 +130,11 @@ defmodule Ash.Schema do
field(aggregate.name, Ash.Type.ecto_type(type), virtual: true)
struct_fields = Keyword.delete(@struct_fields, aggregate.name)
Module.delete_attribute(__MODULE__, :struct_fields)
Module.register_attribute(__MODULE__, :struct_fields, accumulate: true)
Enum.each(struct_fields, &Module.put_attribute(__MODULE__, :struct_fields, &1))
@struct_fields {aggregate.name,
%Ash.NotLoaded{type: :aggregate, field: aggregate.name}}
Module.put_attribute(
__MODULE__,
struct_fields_name,
{aggregate.name, %Ash.NotLoaded{type: :aggregate, field: aggregate.name}}
)
end
for calculation <- Ash.Resource.Info.calculations(__MODULE__) do
@ -123,14 +142,17 @@ defmodule Ash.Schema do
field(calculation.name, Ash.Type.ecto_type(calculation.type), virtual: true)
struct_fields = Keyword.delete(@struct_fields, calculation.name)
Module.delete_attribute(__MODULE__, :struct_fields)
Module.register_attribute(__MODULE__, :struct_fields, accumulate: true)
Enum.each(struct_fields, &Module.put_attribute(__MODULE__, :struct_fields, &1))
@struct_fields {calculation.name,
%Ash.NotLoaded{type: :calculation, field: calculation.name}}
Module.put_attribute(
__MODULE__,
struct_fields_name,
{calculation.name, %Ash.NotLoaded{type: :calculation, field: calculation.name}}
)
end
struct_fields = Keyword.new(Module.get_attribute(__MODULE__, struct_fields_name))
Module.delete_attribute(__MODULE__, struct_fields_name)
Module.register_attribute(__MODULE__, struct_fields_name, accumulate: true)
Enum.each(struct_fields, &Module.put_attribute(__MODULE__, struct_fields_name, &1))
end
end
end

View file

@ -0,0 +1,500 @@
defmodule Ash.ElixirSense.PluginTest do
use ExUnit.Case
def cursors(text) do
{_, cursors} =
ElixirSense.Core.Source.walk_text(text, {false, []}, fn
"#", rest, _, _, {_comment?, cursors} ->
{rest, {true, cursors}}
"\n", rest, _, _, {_comment?, cursors} ->
{rest, {false, cursors}}
"^", rest, line, col, {true, cursors} ->
{rest, {true, [%{line: line - 1, col: col} | cursors]}}
_, rest, _, _, acc ->
{rest, acc}
end)
Enum.reverse(cursors)
end
def suggestions(buffer, cursor) do
ElixirSense.suggestions(buffer, cursor.line, cursor.col)
end
def suggestions(buffer, cursor, type) do
suggestions(buffer, cursor)
|> Enum.filter(fn s -> s.type == type end)
end
def suggestions_by_kind(buffer, cursor, kind) do
suggestions(buffer, cursor)
|> Enum.filter(fn s -> s[:kind] == kind end)
end
test "suggesting DSL items" do
buffer = """
defmodule MyResource do
use Ash.Resource
attrib
# ^
end
"""
[cursor] = cursors(buffer)
result = suggestions(buffer, cursor)
assert [
%{
detail: "Dsl Section",
documentation: doc,
kind: :function,
label: "attributes",
snippet: "attributes do\n $0\nend",
type: :generic
}
] = result
assert doc =~ "attributes"
end
test "suggesting DSL options in a section" do
buffer = """
defmodule MyResource do
@moduledoc false
use Ash.Resource
resource do
descri
# ^
end
end
"""
[cursor] = cursors(buffer)
result = suggestions(buffer, cursor)
assert [
%{
detail: "Option",
documentation:
"A human readable description of the resource, to be used in generated documentation",
kind: :function,
label: "description",
snippet: "description \"$0\"",
type: :generic
}
] = result
end
test "suggesting available sections" do
buffer = """
defmodule do
use Ash.Resource,
extensions: [Ash.Notifier.PubSub]
# ^
end
"""
[cursor] = cursors(buffer)
result = suggestions(buffer, cursor)
labels =
result |> Enum.filter(&Map.has_key?(&1, :label)) |> Enum.map(& &1.label) |> Enum.sort()
assert labels == [
"actions",
"aggregates",
"attributes",
"calculations",
"changes",
"code_interface",
"identities",
"multitenancy",
"preparations",
"pub_sub",
"relationships",
"resource",
"validations"
]
end
test "suggesting available nested sections" do
buffer = """
defmodule MyResource do
use Ash.Resource,
extensions: [Ash.Notifier.PubSub]
actions do
# ^
end
end
"""
[cursor] = cursors(buffer)
result = suggestions(buffer, cursor)
labels = result |> Enum.map(& &1.label) |> Enum.sort()
assert labels == ["create", "defaults", "destroy", "primary_actions?", "read", "update"]
end
test "suggesting available options and entities" do
buffer = """
defmodule MyResource do
use Ash.Resource,
extensions: [Ash.Notifier.PubSub]
actions do
create do
# ^
end
end
end
"""
[cursor] = cursors(buffer)
result = suggestions(buffer, cursor)
labels = result |> Enum.map(& &1.label) |> Enum.sort()
assert labels == [
"accept",
"allow_nil_input",
"argument",
"change",
"description",
"error_handler",
"manual?",
"metadata",
"name",
"primary?",
"reject",
"require_attributes",
"validate"
]
end
test "suggesting available options when in keyword format" do
buffer = """
defmodule MyResource do
use Ash.Resource,
extensions: [Ash.Notifier.PubSub]
attributes do
attribute :name, :type, all
# ^
end
end
"""
[cursor] = cursors(buffer)
result = suggestions(buffer, cursor)
assert [
%{
detail: "Option",
kind: :function,
label: "always_select?",
snippet: "always_select?: true",
type: :generic
},
%{
detail: "Option",
kind: :function,
label: "allow_nil?",
snippet: "allow_nil?: false",
type: :generic
}
] = result
end
test "suggesting available values when in keyword format" do
buffer = """
defmodule MyResource do
use Ash.Resource,
extensions: [Ash.Notifier.PubSub]
attributes do
attribute :name, :type, allow_nil?:
# ^
end
end
"""
[cursor] = cursors(buffer)
result = suggestions(buffer, cursor)
assert [
%{
documentation: "true",
insert_text: "true",
kind: :type_parameter,
label: "boolean",
priority: 0,
snippet: "true",
type: :generic
},
%{
documentation: "false",
insert_text: "false",
kind: :type_parameter,
label: "boolean",
priority: 0,
snippet: "false",
type: :generic
}
] = result
end
test "suggesting available values in section" do
buffer = """
defmodule MyResource do
use Ash.Resource,
extensions: [Ash.Notifier.PubSub]
attributes do
attribute :name, :type do
allow_nil?
# ^
end
end
end
"""
[cursor] = cursors(buffer)
result = suggestions(buffer, cursor)
assert [
%{
documentation: "true",
insert_text: "true",
kind: :type_parameter,
label: "boolean",
priority: 0,
snippet: "true",
type: :generic
},
%{
documentation: "false",
insert_text: "false",
kind: :type_parameter,
label: "boolean",
priority: 0,
snippet: "false",
type: :generic
}
] = result
end
test "suggesting values in args with no hint" do
buffer = """
defmodule MyResource do
use Ash.Resource,
extensions: [Ash.Notifier.PubSub]
pub_sub do
publish_all
# ^
end
end
"""
[cursor] = cursors(buffer)
result = suggestions(buffer, cursor)
assert [
%{
documentation: ":create",
insert_text: ":create",
kind: :type_parameter,
label: "type",
priority: 0,
snippet: ":create",
type: :generic
},
%{
documentation: ":update",
insert_text: ":update",
kind: :type_parameter,
label: "type",
priority: 0,
snippet: ":update",
type: :generic
},
%{
documentation: ":destroy",
insert_text: ":destroy",
kind: :type_parameter,
label: "type",
priority: 0,
snippet: ":destroy",
type: :generic
}
] = result
end
test "suggesting values in args with hint" do
buffer = """
defmodule MyResource do
use Ash.Resource,
extensions: [Ash.Notifier.PubSub]
pub_sub do
publish_all :crea
# ^
end
end
"""
[cursor] = cursors(buffer)
result = suggestions(buffer, cursor)
assert [
%{
documentation: ":create",
insert_text: ":create",
kind: :type_parameter,
label: "type",
priority: 0,
snippet: ":create",
type: :generic
}
] = result
end
test "suggesting ash builtin types" do
buffer = """
defmodule MyResource do
use Ash.Resource,
extensions: [Ash.Notifier.PubSub]
attributes do
attribute :name, :str
# ^
end
end
"""
[cursor] = cursors(buffer)
result = suggestions(buffer, cursor)
assert [
%{
detail: "Ash type",
insert_text: "string",
kind: :type_parameter,
label: ":string",
priority: 0,
snippet: nil,
type: :generic
},
%{
detail: "Ash type",
insert_text: "ci_string",
kind: :type_parameter,
label: ":ci_string",
priority: 0,
snippet: nil,
type: :generic
}
] = result
end
test "suggesting builtin types for behaviours" do
buffer = """
defmodule MyResource do
use Ash.Resource,
extensions: [Ash.Notifier.PubSub]
actions do
create :create do
change manage_relati
# ^
end
end
end
"""
[cursor] = cursors(buffer)
result = suggestions(buffer, cursor)
assert [
%{
args: "argument, relationship_name \\\\ nil, opts",
name: "manage_relationship",
args_list: ["argument", "relationship_name \\\\ nil", "opts"],
arity: 2,
def_arity: 3,
metadata: %{defaults: 1},
origin: "Ash.Resource.Change.Builtins",
snippet: nil,
spec: "",
summary:
"Calls `Ash.Changeset.manage_relationship/4` with the changeset and relationship provided, using the value provided for the named argument",
type: :function,
visibility: :public
},
%{
args: "argument, relationship_name \\\\ nil, opts",
args_list: ["argument", "relationship_name \\\\ nil", "opts"],
arity: 3,
def_arity: 3,
metadata: %{defaults: 1},
name: "manage_relationship",
origin: "Ash.Resource.Change.Builtins",
snippet: nil,
spec: "",
summary:
"Calls `Ash.Changeset.manage_relationship/4` with the changeset and relationship provided, using the value provided for the named argument",
type: :function,
visibility: :public
}
] = result
end
test "suggesting available sections from single extension types" do
buffer = """
defmodule do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets
ets do
# ^
end
end
"""
[cursor] = cursors(buffer)
result = suggestions(buffer, cursor)
assert result == [
%{
detail: "Option",
documentation: nil,
kind: :function,
label: "private?",
snippet: "private? true",
type: :generic
}
]
end
end