From 9a44167aeb06eac5a5e8cedf29b0a0ec158a8546 Mon Sep 17 00:00:00 2001 From: Josh Price Date: Sat, 13 Sep 2025 17:11:19 +0200 Subject: [PATCH 1/2] Raise helpful errors for DSL section inline and inline+block syntax Currently DSL sections throw confusing error messages for the following DSL declarations: 1. Using inline keyword args instead of block syntax gives ``` ** (Spark.Options.ValidationError) required :first_name option not found, received options: [] ``` 2. Using inline and block syntax throws the following compile error ``` error: undefined function section_name/2 (there is no such import) ``` We now catch these errors and show helpful errors so that the user understands what went wrong and how to fix it. The new errors are as follows: 1. Inline syntax ``` Cannot use inline syntax for DSL section `personal_details`. Use block syntax: `personal_details do ... end`. ``` 2. Mixed inline and block syntax ``` Cannot use both inline syntax and block syntax for DSL section `personal_details`. Use block syntax `personal_details do ... end` ``` --- lib/spark/dsl/extension.ex | 32 +++++++++++++++++++++++++++++--- test/dsl_test.exs | 30 ++++++++++++++++++++++++++++++ test/support/contact/contact.ex | 2 +- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/lib/spark/dsl/extension.ex b/lib/spark/dsl/extension.ex index 0b015836..de3c2d6d 100644 --- a/lib/spark/dsl/extension.ex +++ b/lib/spark/dsl/extension.ex @@ -471,7 +471,7 @@ defmodule Spark.Dsl.Extension do only_sections = sections |> Enum.reject(& &1.top_level?) - |> Enum.map(&{&1.name, 1}) + |> Enum.flat_map(&[{&1.name, 1}, {&1.name, 2}]) quote generated: true, location: :keep do require Spark.Dsl.Extension @@ -878,11 +878,37 @@ defmodule Spark.Dsl.Extension do end unless section.top_level? && path == [] do - defmacro unquote(section.name)(body) do + # Handle mixed syntax - personal_details(opts, do: block) + defmacro unquote(section.name)(opts, [do: _] = _block) do + section_name = unquote(section.name) + + raise Spark.Error.DslError, + module: __CALLER__.module, + message: + "Cannot use both inline syntax and block syntax for DSL section `#{section_name}`. " <> + "Use block syntax `#{section_name} do ... end`", + path: unquote(path ++ [section.name]) + end + + # Handle inline syntax and normal block syntax + defmacro unquote(section.name)(body_or_opts) do + section_name = unquote(section.name) + + # Check if this is inline syntax (keyword list without :do) + if is_list(body_or_opts) and not Keyword.has_key?(body_or_opts, :do) do + raise Spark.Error.DslError, + module: __CALLER__.module, + message: + "Cannot use inline syntax for DSL section `#{section_name}`. " <> + "Use block syntax: `#{section_name} do ... end`.", + path: unquote(path ++ [section.name]) + end + + body = body_or_opts + opts_module = unquote(opts_module) section_path = unquote(path ++ [section.name]) extension = unquote(extension) - entity_modules = unquote(entity_modules) patch_modules = diff --git a/test/dsl_test.exs b/test/dsl_test.exs index 8dd65dc6..0cfc25de 100644 --- a/test/dsl_test.exs +++ b/test/dsl_test.exs @@ -396,6 +396,36 @@ defmodule Spark.DslTest do end end + describe "DSL section syntax validation" do + test "mixed inline/block syntax should provide helpful error" do + assert_raise Spark.Error.DslError, + ~r/Cannot use both inline syntax and block syntax.*personal_details/, + fn -> + defmodule MixedSyntax do + @moduledoc false + use Spark.Test.Contact + + personal_details first_name: "Luke" do + last_name("Skywalker") + end + end + end + end + + test "inline syntax for DSL section should provide helpful error" do + assert_raise Spark.Error.DslError, + ~r/Cannot use inline syntax for DSL section.*personal_details/, + fn -> + defmodule InlineSyntax do + @moduledoc false + use Spark.Test.Contact + + personal_details(first_name: "Luke", last_name: "Skywalker") + end + end + end + end + describe "optional entity arguments" do test "optional arguments can be supplied either as arguments or as dsl options" do defmodule OptionEinstein do diff --git a/test/support/contact/contact.ex b/test/support/contact/contact.ex index be29eb6f..312f1a48 100644 --- a/test/support/contact/contact.ex +++ b/test/support/contact/contact.ex @@ -15,7 +15,7 @@ defmodule Spark.Test.Contact do doc: "A module" ], contacter: [ - doc: "A function that wil contact this person with a message", + doc: "A function that will contact this person with a message", type: {:spark_function_behaviour, Spark.Test.Contact.Contacter, Spark.Test.ContacterBuiltins, {Spark.Test.Contact.Contacter.Function, 1}} From d1d796e91c47caf7f109764b369b01972a863246 Mon Sep 17 00:00:00 2001 From: Josh Price Date: Mon, 15 Sep 2025 21:46:33 +0800 Subject: [PATCH 2/2] improvement: helpful error message when mixing entity inline/block syntax --- lib/spark/dsl/extension.ex | 19 +++++++++++++++++++ test/dsl_test.exs | 19 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/spark/dsl/extension.ex b/lib/spark/dsl/extension.ex index de3c2d6d..798bfce5 100644 --- a/lib/spark/dsl/extension.ex +++ b/lib/spark/dsl/extension.ex @@ -1252,6 +1252,25 @@ defmodule Spark.Dsl.Extension do end @moduledoc false + + # Generate mixed syntax macro only for entities without optional args + if not Enum.any?(entity.args, fn + {:optional, _, _} -> true + {:optional, _} -> true + _ -> false + end) do + defmacro unquote(entity.name)(unquote_splicing(args), extra_opts, [do: _] = _block) do + entity_name = unquote(entity.name) + + raise Spark.Error.DslError, + module: __CALLER__.module, + message: + "Cannot use both inline syntax and block syntax for entity `#{entity_name}`. " <> + "Use block syntax `#{entity_name} args... do ... end`", + path: unquote(section_path ++ nested_entity_path) + end + end + defmacro unquote(entity.name)(unquote_splicing(args), opts \\ nil) do section_path = unquote(Macro.escape(section_path)) entity_schema = unquote(Macro.escape(entity.schema)) diff --git a/test/dsl_test.exs b/test/dsl_test.exs index 0cfc25de..bc0f495c 100644 --- a/test/dsl_test.exs +++ b/test/dsl_test.exs @@ -396,7 +396,7 @@ defmodule Spark.DslTest do end end - describe "DSL section syntax validation" do + describe "DSL section and entity syntax validation" do test "mixed inline/block syntax should provide helpful error" do assert_raise Spark.Error.DslError, ~r/Cannot use both inline syntax and block syntax.*personal_details/, @@ -424,6 +424,23 @@ defmodule Spark.DslTest do end end end + + test "mixed inline/block syntax for entity should provide helpful error" do + assert_raise Spark.Error.DslError, + ~r/Cannot use both inline syntax and block syntax.*preset/, + fn -> + defmodule EntityMixedSyntax do + @moduledoc false + use Spark.Test.Contact + + presets do + preset :einstein, default_message: "E=mc²" do + contacter(fn x -> x end) + end + end + end + end + end end describe "optional entity arguments" do