defmodule Phoenix.Component.Declarative do @moduledoc false ## Reserved assigns # This list should only contain attributes that are given to components by engines # @socket, @myself, etc. should not be listed here, as they shouldn't be given to # function components in the first place @reserved_assigns [:__changed__, :__slot__, :__given__, :inner_block] @doc false def __reserved__, do: @reserved_assigns ## Global @global_prefixes ~w( phx- aria- data- ) @globals ~w( accesskey alt autocapitalize autofocus class contenteditable contextmenu dir draggable enterkeyhint exportparts height hidden id inert inputmode is itemid itemprop itemref itemscope itemtype lang nonce onabort onautocomplete onautocompleteerror onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextmenu oncuechange ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended onerror onfocus oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart onmousedown onmouseenter onmouseleave onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay onplaying onprogress onratechange onreset onresize onscroll onseeked onseeking onselect onshow onsort onstalled onsubmit onsuspend ontimeupdate ontoggle onvolumechange onwaiting part placeholder rel role slot spellcheck style tabindex target title translate type width xml:base xml:lang ) @doc false def __global__?(module, name, global_attr) when is_atom(module) and is_binary(name) do includes = Keyword.get(global_attr.opts, :include, []) if function_exported?(module, :__global__?, 1) do module.__global__?(name) or __global__?(name) or name in includes else __global__?(name) or name in includes end end for prefix <- @global_prefixes do def __global__?(unquote(prefix) <> _), do: true end for name <- @globals do def __global__?(unquote(name)), do: true end def __global__?(_), do: false ## Def overrides @doc false defmacro def(expr, body) do quote do Kernel.def(unquote(annotate_def(:def, expr)), unquote(body)) end end @doc false defmacro defp(expr, body) do quote do Kernel.defp(unquote(annotate_def(:defp, expr)), unquote(body)) end end defp annotate_def(kind, expr) do case expr do {:when, meta, [left, right]} -> {:when, meta, [annotate_call(kind, left), right]} left -> annotate_call(kind, left) end end defp annotate_call(kind, {name, meta, [{:\\, default_meta, [left, right]}]}), do: {name, meta, [{:\\, default_meta, [annotate_arg(kind, left), right]}]} defp annotate_call(kind, {name, meta, [arg]}), do: {name, meta, [annotate_arg(kind, arg)]} defp annotate_call(_kind, left), do: left defp annotate_arg(kind, {:=, meta, [{name, _, ctx} = var, arg]}) when is_atom(name) and is_atom(ctx) do {:=, meta, [var, quote(do: unquote(__MODULE__).__pattern__!(unquote(kind), unquote(arg)))]} end defp annotate_arg(kind, {:=, meta, [arg, {name, _, ctx} = var]}) when is_atom(name) and is_atom(ctx) do {:=, meta, [quote(do: unquote(__MODULE__).__pattern__!(unquote(kind), unquote(arg))), var]} end defp annotate_arg(kind, {name, meta, ctx} = var) when is_atom(name) and is_atom(ctx) do {:=, meta, [quote(do: unquote(__MODULE__).__pattern__!(unquote(kind), _)), var]} end defp annotate_arg(kind, arg) do quote(do: unquote(__MODULE__).__pattern__!(unquote(kind), unquote(arg))) end ## Attrs/slots @doc false @valid_opts [:global_prefixes] def __setup__(module, opts) do {prefixes, invalid_opts} = Keyword.pop(opts, :global_prefixes, []) prefix_matches = for prefix <- prefixes do unless String.ends_with?(prefix, "-") do raise ArgumentError, "global prefixes for #{inspect(module)} must end with a dash, got: #{inspect(prefix)}" end quote(do: {unquote(prefix) <> _, true}) end if invalid_opts != [] do raise ArgumentError, """ invalid options passed to #{inspect(__MODULE__)}. The following options are supported: #{inspect(@valid_opts)}, got: #{inspect(invalid_opts)} """ end Module.register_attribute(module, :__attrs__, accumulate: true) Module.register_attribute(module, :__slot_attrs__, accumulate: true) Module.register_attribute(module, :__slots__, accumulate: true) Module.register_attribute(module, :__slot__, accumulate: false) Module.register_attribute(module, :__components_calls__, accumulate: true) Module.put_attribute(module, :__components__, %{}) Module.put_attribute(module, :on_definition, __MODULE__) Module.put_attribute(module, :before_compile, __MODULE__) if prefix_matches == [] do [] else prefix_matches ++ [quote(do: {_, false})] end end @doc false def __slot__!(module, name, opts, line, file, block_fun) do ensure_used!(module, line, file) {doc, opts} = Keyword.pop(opts, :doc, nil) unless is_binary(doc) or is_nil(doc) or doc == false do compile_error!(line, file, ":doc must be a string or false, got: #{inspect(doc)}") end {required, opts} = Keyword.pop(opts, :required, false) {validate_attrs, opts} = Keyword.pop(opts, :validate_attrs, true) unless is_boolean(required) do compile_error!(line, file, ":required must be a boolean, got: #{inspect(required)}") end Module.put_attribute(module, :__slot__, name) slot_attrs = try do block_fun.() module |> Module.get_attribute(:__slot_attrs__) |> Enum.reverse() after Module.put_attribute(module, :__slot__, nil) Module.delete_attribute(module, :__slot_attrs__) end slot = %{ name: name, required: required, opts: opts, doc: doc, line: line, attrs: slot_attrs, validate_attrs: validate_attrs } validate_slot!(module, slot, line, file) Module.put_attribute(module, :__slots__, slot) :ok end defp validate_slot!(module, slot, line, file) do slots = Module.get_attribute(module, :__slots__) || [] if Enum.find(slots, &(&1.name == slot.name)) do compile_error!(line, file, """ a duplicate slot with name #{inspect(slot.name)} already exists\ """) end if slot.name == :inner_block and slot.attrs != [] do compile_error!(line, file, """ cannot define attributes in a slot with name #{inspect(slot.name)} """) end end @doc false def __attr__!(module, name, type, opts, line, file) when is_atom(name) and is_list(opts) do ensure_used!(module, line, file) slot = Module.get_attribute(module, :__slot__) if name == :inner_block do compile_error!( line, file, "cannot define attribute called :inner_block. Maybe you wanted to use `slot` instead?" ) end if type == :global && slot do compile_error!(line, file, "cannot define :global slot attributes") end if type == :global and Keyword.has_key?(opts, :required) do compile_error!(line, file, "global attributes do not support the :required option") end if type == :global and Keyword.has_key?(opts, :values) do compile_error!(line, file, "global attributes do not support the :values option") end if type == :global and Keyword.has_key?(opts, :examples) do compile_error!(line, file, "global attributes do not support the :examples option") end if type != :global and Keyword.has_key?(opts, :include) do compile_error!(line, file, ":include is only supported for :global attributes") end {doc, opts} = Keyword.pop(opts, :doc, nil) unless is_binary(doc) or is_nil(doc) or doc == false do compile_error!(line, file, ":doc must be a string or false, got: #{inspect(doc)}") end {required, opts} = Keyword.pop(opts, :required, false) unless is_boolean(required) do compile_error!(line, file, ":required must be a boolean, got: #{inspect(required)}") end if required and Keyword.has_key?(opts, :default) do compile_error!(line, file, "only one of :required or :default must be given") end key = if slot, do: :__slot_attrs__, else: :__attrs__ type = validate_attr_type!(module, key, slot, name, type, line, file) validate_attr_opts!(slot, name, opts, line, file) if Keyword.has_key?(opts, :values) and Keyword.has_key?(opts, :examples) do compile_error!(line, file, "only one of :values or :examples must be given") end if Keyword.has_key?(opts, :values) do validate_attr_values!(slot, name, type, opts[:values], line, file) end if Keyword.has_key?(opts, :examples) do validate_attr_examples!(slot, name, type, opts[:examples], line, file) end if Keyword.has_key?(opts, :default) do validate_attr_default!(slot, name, type, opts, line, file) end attr = %{ slot: slot, name: name, type: type, required: required, opts: opts, doc: doc, line: line } Module.put_attribute(module, key, attr) :ok end @builtin_types [:boolean, :integer, :float, :string, :atom, :list, :map, :global] @valid_types [:any] ++ @builtin_types defp validate_attr_type!(module, key, slot, name, type, line, file) when is_atom(type) do attrs = Module.get_attribute(module, key) || [] cond do Enum.find(attrs, fn attr -> attr.name == name end) -> compile_error!(line, file, """ a duplicate attribute with name #{attr_slot(name, slot)} already exists\ """) existing = type == :global && Enum.find(attrs, fn attr -> attr.type == :global end) -> compile_error!(line, file, """ cannot define :global attribute #{inspect(name)} because one \ is already defined as #{attr_slot(existing.name, slot)}. \ Only a single :global attribute may be defined\ """) true -> :ok end case Atom.to_string(type) do "Elixir." <> _ -> {:struct, type} _ when type in @valid_types -> type _ -> bad_type!(slot, name, type, line, file) end end defp validate_attr_type!(_module, _key, slot, name, type, line, file) do bad_type!(slot, name, type, line, file) end defp bad_type!(slot, name, type, line, file) do compile_error!(line, file, """ invalid type #{inspect(type)} for attr #{attr_slot(name, slot)}. \ The following types are supported: * any Elixir struct, such as URI, MyApp.User, etc * one of #{Enum.map_join(@builtin_types, ", ", &inspect/1)} * :any for all other types """) end defp attr_slot(name, nil), do: "#{inspect(name)}" defp attr_slot(name, slot), do: "#{inspect(name)} in slot #{inspect(slot)}" defp validate_attr_default!(slot, name, type, opts, line, file) do case {opts[:default], opts[:values]} do {default, nil} -> unless valid_value?(type, default) do bad_default!(slot, name, type, default, line, file) end {default, values} -> unless default in values do compile_error!(line, file, """ expected the default value for attr #{attr_slot(name, slot)} to be one of #{inspect(values)}, \ got: #{inspect(default)} """) end end end defp bad_default!(slot, name, type, default, line, file) do compile_error!(line, file, """ expected the default value for attr #{attr_slot(name, slot)} to be #{type_with_article(type)}, \ got: #{inspect(default)} """) end defp validate_attr_values!(slot, name, type, values, line, file) do unless is_enumerable(values) and not Enum.empty?(values) do compile_error!(line, file, """ :values must be a non-empty enumerable, got: #{inspect(values)} """) end for value <- values, not valid_value?(type, value), do: bad_value!(slot, name, type, value, line, file) end defp is_enumerable(values) do Enumerable.impl_for(values) != nil end defp bad_value!(slot, name, type, value, line, file) do compile_error!(line, file, """ expected the values for attr #{attr_slot(name, slot)} to be #{type_with_article(type)}, \ got: #{inspect(value)} """) end defp validate_attr_examples!(slot, name, type, examples, line, file) do unless is_list(examples) and not Enum.empty?(examples) do compile_error!(line, file, """ :examples must be a non-empty list, got: #{inspect(examples)} """) end for example <- examples, not valid_value?(type, example), do: bad_example!(slot, name, type, example, line, file) end defp bad_example!(slot, name, type, example, line, file) do compile_error!(line, file, """ expected the examples for attr #{attr_slot(name, slot)} to be #{type_with_article(type)}, \ got: #{inspect(example)} """) end defp valid_value?(_type, nil), do: true defp valid_value?(:any, _value), do: true defp valid_value?(:string, value), do: is_binary(value) defp valid_value?(:atom, value), do: is_atom(value) defp valid_value?(:boolean, value), do: is_boolean(value) defp valid_value?(:integer, value), do: is_integer(value) defp valid_value?(:float, value), do: is_float(value) defp valid_value?(:list, value), do: is_list(value) defp valid_value?({:struct, mod}, value), do: is_struct(value, mod) defp valid_value?(_type, _value), do: true defp validate_attr_opts!(slot, name, opts, line, file) do for {key, _} <- opts, message = invalid_attr_message(key, slot) do compile_error!(line, file, """ invalid option #{inspect(key)} for attr #{attr_slot(name, slot)}. #{message}\ """) end end defp invalid_attr_message(:include, inc) when is_list(inc) or is_nil(inc), do: nil defp invalid_attr_message(:include, other), do: "include only supports a list of attributes, got: #{inspect(other)}" defp invalid_attr_message(:default, nil), do: nil defp invalid_attr_message(:default, _), do: ":default is not supported inside slot attributes, " <> "instead use Map.get/3 with a default value when accessing a slot attribute" defp invalid_attr_message(:required, _), do: nil defp invalid_attr_message(:values, _), do: nil defp invalid_attr_message(:examples, _), do: nil defp invalid_attr_message(_key, nil), do: "The supported options are: [:required, :default, :values, :examples]" defp invalid_attr_message(_key, _slot), do: "The supported options inside slots are: [:required]" defp compile_error!(line, file, msg) do raise CompileError, line: line, file: file, description: msg end defmacro __pattern__!(kind, arg) do {name, 1} = __CALLER__.function {_slots, attrs} = register_component!(kind, __CALLER__, name, true) fields = for %{name: name, required: true, type: {:struct, struct}} <- attrs do {name, quote(do: %unquote(struct){})} end if fields == [] do arg else quote(do: %{unquote_splicing(fields)} = unquote(arg)) end end @doc false def __on_definition__(env, kind, name, args, _guards, body) do check? = not String.starts_with?(to_string(name), "__") cond do check? and length(args) == 1 and body == nil -> register_component!(kind, env, name, false) check? -> attrs = pop_attrs(env) validate_misplaced_attrs!(attrs, env.file, fn -> case length(args) do 1 -> "could not define attributes for function #{name}/1. " <> "Please make sure that you have `use Phoenix.Component` and that the function has no default arguments" arity -> "cannot declare attributes for function #{name}/#{arity}. Components must be functions with arity 1" end end) slots = pop_slots(env) validate_misplaced_slots!(slots, env.file, fn -> case length(args) do 1 -> "could not define slots for function #{name}/1. " <> "Components cannot be dynamically defined or have default arguments" arity -> "cannot declare slots for function #{name}/#{arity}. Components must be functions with arity 1" end end) true -> :ok end end @after_verify_supported Version.match?(System.version(), ">= 1.14.0-dev") @doc false defmacro __before_compile__(env) do attrs = pop_attrs(env) validate_misplaced_attrs!(attrs, env.file, fn -> "cannot define attributes without a related function component" end) slots = pop_slots(env) validate_misplaced_slots!(slots, env.file, fn -> "cannot define slots without a related function component" end) components = Module.get_attribute(env.module, :__components__) components_calls = Module.get_attribute(env.module, :__components_calls__) |> Enum.reverse() names_and_defs = for {name, %{kind: kind, attrs: attrs, slots: slots, line: line}} <- components do attr_defaults = for %{name: name, required: false, opts: opts} <- attrs, Keyword.has_key?(opts, :default), do: {name, Macro.escape(opts[:default])} slot_defaults = for %{name: name, required: false} <- slots do {name, []} end defaults = attr_defaults ++ slot_defaults {global_name, global_default} = case Enum.find(attrs, fn attr -> attr.type == :global end) do %{name: name, opts: opts} -> {name, Macro.escape(Keyword.get(opts, :default, %{}))} nil -> {nil, nil} end attr_names = for(attr <- attrs, do: attr.name) slot_names = for(slot <- slots, do: slot.name) known_keys = attr_names ++ slot_names ++ @reserved_assigns def_body = if global_name do quote do {assigns, caller_globals} = Map.split(assigns, unquote(known_keys)) globals = case assigns do %{unquote(global_name) => explicit_global_assign} -> explicit_global_assign %{} -> Map.merge(unquote(global_default), caller_globals) end merged = %{unquote_splicing(defaults)} |> Map.merge(assigns) |> Map.put(:__given__, assigns) super(Phoenix.Component.assign(merged, unquote(global_name), globals)) end else quote do merged = %{unquote_splicing(defaults)} |> Map.merge(assigns) |> Map.put(:__given__, assigns) super(merged) end end merge = quote line: line do Kernel.unquote(kind)(unquote(name)(assigns)) do unquote(def_body) end end {{name, 1}, merge} end {names, defs} = Enum.unzip(names_and_defs) overridable = if names != [] do quote do defoverridable unquote(names) end end def_components_ast = quote do def __components__() do unquote(Macro.escape(components)) end end def_components_calls_ast = if components_calls != [] and @after_verify_supported do quote do @after_verify {__MODULE__, :__phoenix_component_verify__} @doc false def __phoenix_component_verify__(module) do unquote(__MODULE__).__verify__(module, unquote(Macro.escape(components_calls))) end end end {:__block__, [], [def_components_ast, def_components_calls_ast, overridable | defs]} end defp register_component!(kind, env, name, check_if_defined?) do slots = pop_slots(env) attrs = pop_attrs(env) cond do slots != [] or attrs != [] -> check_if_defined? and raise_if_function_already_defined!(env, name, slots, attrs) register_component_doc(env, kind, slots, attrs) for %{name: slot_name, line: line} <- slots, Enum.find(attrs, &(&1.name == slot_name)) do compile_error!(line, env.file, """ cannot define a slot with name #{inspect(slot_name)}, as an attribute with that name already exists\ """) end components = env.module |> Module.get_attribute(:__components__) # Sort by name as this is used when they are validated |> Map.put(name, %{ kind: kind, attrs: Enum.sort_by(attrs, & &1.name), slots: Enum.sort_by(slots, & &1.name), line: env.line }) Module.put_attribute(env.module, :__components__, components) Module.put_attribute(env.module, :__last_component__, name) {slots, attrs} Module.get_attribute(env.module, :__last_component__) == name -> %{slots: slots, attrs: attrs} = Module.get_attribute(env.module, :__components__)[name] {slots, attrs} true -> {[], []} end end # Documentation handling defp register_component_doc(env, :def, slots, attrs) do case Module.get_attribute(env.module, :doc) do {_line, false} -> :ok {line, doc} -> Module.put_attribute(env.module, :doc, {line, build_component_doc(doc, slots, attrs)}) nil -> Module.put_attribute(env.module, :doc, {env.line, build_component_doc(slots, attrs)}) end end defp register_component_doc(_env, :defp, _slots, _attrs) do :ok end defp build_component_doc(doc \\ "", slots, attrs) do [left | right] = String.split(doc, "[INSERT LVATTRDOCS]") IO.iodata_to_binary([ build_left_doc(left), build_component_docs(slots, attrs), build_right_doc(right) ]) end defp build_left_doc("") do [""] end defp build_left_doc(left) do [left, ?\n] end defp build_component_docs(slots, attrs) do case {slots, attrs} do {[], []} -> [] {slots, [] = _attrs} -> [build_slots_docs(slots)] {[] = _slots, attrs} -> [build_attrs_docs(attrs)] {slots, attrs} -> [build_attrs_docs(attrs), ?\n, build_slots_docs(slots)] end end defp build_slots_docs(slots) do [ "## Slots\n", for slot <- slots, slot.doc != false, into: [] do slot_attrs = for slot_attr <- slot.attrs, slot_attr.doc != false, slot_attr.slot == slot.name, do: slot_attr [ "\n* ", build_slot_name(slot), build_slot_required(slot), build_slot_doc(slot, slot_attrs) ] end ] end defp build_attrs_docs(attrs) do [ "## Attributes\n", for attr <- attrs, attr.doc != false and attr.type != :global do [ "\n* ", build_attr_name(attr), build_attr_type(attr), build_attr_required(attr), build_hyphen(attr), build_attr_doc_and_default(attr, " "), build_attr_values_or_examples(attr) ] end, # global always goes at the end case Enum.find(attrs, &(&1.type === :global)) do nil -> [] attr -> build_attr_doc_and_default(attr, " ") end ] end defp build_slot_name(%{name: name}) do ["`", Atom.to_string(name), "`"] end defp build_slot_doc(%{doc: nil}, []) do [] end defp build_slot_doc(%{doc: doc}, []) do [" - ", build_doc(doc, " ", false)] end defp build_slot_doc(%{doc: nil}, slot_attrs) do [" - Accepts attributes:\n", build_slot_attrs_docs(slot_attrs)] end defp build_slot_doc(%{doc: doc}, slot_attrs) do [ " - ", build_doc(doc, " ", true), "Accepts attributes:\n", build_slot_attrs_docs(slot_attrs) ] end defp build_slot_attrs_docs(slot_attrs) do for slot_attr <- slot_attrs do [ "\n * ", build_attr_name(slot_attr), build_attr_type(slot_attr), build_attr_required(slot_attr), build_hyphen(slot_attr), build_attr_doc_and_default(slot_attr, " "), build_attr_values_or_examples(slot_attr) ] end end defp build_slot_required(%{required: true}) do [" (required)"] end defp build_slot_required(_slot) do [] end defp build_attr_name(%{name: name}) do ["`", Atom.to_string(name), "` "] end defp build_attr_type(%{type: {:struct, type}}) do ["(`", inspect(type), "`)"] end defp build_attr_type(%{type: type}) do ["(`", inspect(type), "`)"] end defp build_attr_required(%{required: true}) do [" (required)"] end defp build_attr_required(_attr) do [] end defp build_attr_doc_and_default(%{doc: doc, type: :global, opts: opts}, indent) do [ "\n* Global attributes are accepted.", if(doc, do: [" ", build_doc(doc, indent, false)], else: []), case Keyword.get(opts, :include) do inc when is_list(inc) and inc != [] -> [" ", "Supports all globals plus:", " ", build_literal(inc), "."] _ -> [] end ] end defp build_attr_doc_and_default(%{doc: doc, opts: opts}, indent) do case Keyword.fetch(opts, :default) do {:ok, default} -> if doc do [build_doc(doc, indent, true), "Defaults to ", build_literal(default), "."] else ["Defaults to ", build_literal(default), "."] end :error -> if doc, do: [build_doc(doc, indent, false)], else: [] end end defp build_doc(doc, indent, text_after?) do doc = String.trim(doc) [head | tail] = String.split(doc, ["\r\n", "\n"]) dot = if String.ends_with?(doc, "."), do: [], else: [?.] tail = Enum.map(tail, fn "" -> "\n" other -> [?\n, indent | other] end) case tail do # Single line [] when text_after? -> [[head | tail], dot, ?\s] [] -> [[head | tail], dot] # Multi-line _ when text_after? -> [[head | tail], "\n\n", indent] _ -> [[head | tail], "\n"] end end defp build_attr_values_or_examples(%{opts: [values: values]}) do ["Must be one of ", build_literals_list(values, "or"), ?.] end defp build_attr_values_or_examples(%{opts: [examples: examples]}) do ["Examples include ", build_literals_list(examples, "and"), ?.] end defp build_attr_values_or_examples(_attr) do [] end defp build_literals_list([literal], _condition) do [build_literal(literal)] end defp build_literals_list(literals, condition) do literals |> Enum.map_intersperse(", ", &build_literal/1) |> List.insert_at(-2, [condition, " "]) end defp build_literal(literal) do [?`, inspect(literal, charlists: :as_list), ?`] end defp build_hyphen(%{doc: doc}) when is_binary(doc) do [" - "] end defp build_hyphen(%{opts: []}) do [] end defp build_hyphen(%{opts: _opts}) do [" - "] end defp build_right_doc("") do [] end defp build_right_doc(right) do [?\n, right] end defp validate_misplaced_attrs!(attrs, file, message_fun) do with [%{line: first_attr_line} | _] <- attrs do compile_error!(first_attr_line, file, message_fun.()) end end defp validate_misplaced_slots!(slots, file, message_fun) do with [%{line: first_slot_line} | _] <- slots do compile_error!(first_slot_line, file, message_fun.()) end end defp pop_attrs(env) do slots = Module.delete_attribute(env.module, :__attrs__) || [] Enum.reverse(slots) end defp pop_slots(env) do slots = Module.delete_attribute(env.module, :__slots__) || [] Enum.reverse(slots) end defp raise_if_function_already_defined!(env, name, slots, attrs) do if Module.defines?(env.module, {name, 1}) do {:v1, _, meta, _} = Module.get_definition(env.module, {name, 1}) with [%{line: first_attr_line} | _] <- attrs do compile_error!(first_attr_line, env.file, """ attributes must be defined before the first function clause at line #{meta[:line]} """) end with [%{line: first_slot_line} | _] <- slots do compile_error!(first_slot_line, env.file, """ slots must be defined before the first function clause at line #{meta[:line]} """) end end end # Verification @doc false def __verify__(module, component_calls) do for %{component: {submod, fun}} = call <- component_calls, function_exported?(submod, :__components__, 0), component = submod.__components__()[fun], do: verify(module, call, component) :ok end defp verify( caller_module, %{slots: slots, attrs: attrs, root: root} = call, %{slots: slots_defs, attrs: attrs_defs} = _component ) do {attrs, global_attr} = Enum.reduce(attrs_defs, {attrs, nil}, fn attr_def, {attrs, global_attr} -> %{name: name, required: required, type: type, opts: opts} = attr_def attr_values = Keyword.get(opts, :values, nil) {value, attrs} = Map.pop(attrs, name) case {type, value} do # missing required attr {_type, nil} when not root and required -> message = "missing required attribute \"#{name}\" for component #{component_fa(call)}" warn(message, call.file, call.line) # missing optional attr, or dynamic attr {_type, nil} when root or not required -> :ok # global attrs cannot be directly used {:global, {line, _column, _type_value}} -> message = "global attribute \"#{name}\" in component #{component_fa(call)} may not be provided directly" warn(message, call.file, line) # attrs must be one of values {_type, {line, _column, {_, type_value}}} when not is_nil(attr_values) -> unless type_value in attr_values do message = "attribute \"#{name}\" in component #{component_fa(call)} must be one of #{inspect(attr_values)}, got: #{inspect(type_value)}" warn(message, call.file, line) end # attrs must be of the declared type {type, {line, _column, type_value}} -> if value_ast_to_string = type_mismatch(type, type_value) do message = "attribute \"#{name}\" in component #{component_fa(call)} must be #{type_with_article(type)}, got: " <> value_ast_to_string [warn(message, call.file, line)] end end {attrs, global_attr || (type == :global and attr_def)} end) for {name, {line, _column, _type_value}} <- attrs, !(global_attr && __global__?(caller_module, Atom.to_string(name), global_attr)) do message = "undefined attribute \"#{name}\" for component #{component_fa(call)}" warn(message, call.file, line) end undefined_slots = Enum.reduce(slots_defs, slots, fn slot_def, slots -> %{name: slot_name, required: required, attrs: attrs, validate_attrs: validate_attrs} = slot_def {slot_values, slots} = Map.pop(slots, slot_name) case slot_values do # missing required slot nil when required -> message = "missing required slot \"#{slot_name}\" for component #{component_fa(call)}" warn(message, call.file, call.line) # missing optional slot nil -> :ok # slot with attributes _ -> slot_attr_defs = Enum.into(attrs, %{}, &{&1.name, &1}) required_attrs = for {attr_name, %{required: true}} <- slot_attr_defs, do: attr_name for %{attrs: slot_attrs, line: slot_line, root: false} <- slot_values, attr_name <- required_attrs, not Map.has_key?(slot_attrs, attr_name) do message = "missing required attribute \"#{attr_name}\" in slot \"#{slot_name}\" " <> "for component #{component_fa(call)}" warn(message, call.file, slot_line) end for %{attrs: slot_attrs} <- slot_values, {attr_name, {line, _column, type_value}} <- slot_attrs do case slot_attr_defs do # slots cannot accept global attributes %{^attr_name => %{type: :global}} -> message = "global attribute \"#{attr_name}\" in slot \"#{slot_name}\" " <> "for component #{component_fa(call)} may not be provided directly" warn(message, call.file, line) # slot attrs must be one of values %{^attr_name => %{type: _type, opts: [values: attr_values]}} when is_tuple(type_value) and tuple_size(type_value) == 2 -> {_, attr_value} = type_value unless attr_value in attr_values do message = "attribute \"#{attr_name}\" in slot \"#{slot_name}\" " <> "for component #{component_fa(call)} must be one of #{inspect(attr_values)}, got: " <> inspect(attr_value) warn(message, call.file, line) end # slot attrs must be of the declared type %{^attr_name => %{type: type}} -> if value_ast_to_string = type_mismatch(type, type_value) do message = "attribute \"#{attr_name}\" in slot \"#{slot_name}\" " <> "for component #{component_fa(call)} must be #{type_with_article(type)}, got: " <> value_ast_to_string warn(message, call.file, line) end # undefined slot attr %{} -> cond do attr_name == :inner_block -> :ok attrs == [] and not validate_attrs -> :ok true -> message = "undefined attribute \"#{attr_name}\" in slot \"#{slot_name}\" " <> "for component #{component_fa(call)}" warn(message, call.file, line) end end end end slots end) for {slot_name, slot_values} <- undefined_slots, %{line: line} <- slot_values, not implicit_inner_block?(slot_name, slots_defs) do message = "undefined slot \"#{slot_name}\" for component #{component_fa(call)}" warn(message, call.file, line) end :ok end defp implicit_inner_block?(slot_name, slots_defs) do slot_name == :inner_block and length(slots_defs) > 0 end defp type_mismatch(:any, _type_value), do: nil defp type_mismatch(_type, :any), do: nil defp type_mismatch(type, {type, _value}), do: nil defp type_mismatch(:atom, {:boolean, _value}), do: nil defp type_mismatch({:struct, _}, {:map, {:%{}, _, [{:|, _, [_, _]}]}}), do: nil defp type_mismatch(_type, {_, value}), do: Macro.to_string(value) defp component_fa(%{component: {mod, fun}}) do "#{inspect(mod)}.#{fun}/1" end ## Shared helpers defp type_with_article({:struct, struct}), do: "a #{inspect(struct)} struct" defp type_with_article(type) when type in [:atom, :integer], do: "an #{inspect(type)}" defp type_with_article(type), do: "a #{inspect(type)}" # TODO: Provide column information in error messages defp warn(message, file, line) do IO.warn(message, file: file, line: line) end defp ensure_used!(module, line, file) do if !Module.get_attribute(module, :__attrs__) do compile_error!( line, file, "you must `use Phoenix.Component` to declare attributes. It is currently only imported." ) end end end