diff --git a/apps/engine/mix.lock b/apps/engine/mix.lock index 3e860a81..0066c788 100644 --- a/apps/engine/mix.lock +++ b/apps/engine/mix.lock @@ -29,6 +29,7 @@ "schematic": {:hex, :schematic, "0.2.1", "0b091df94146fd15a0a343d1bd179a6c5a58562527746dadd09477311698dbb1", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0b255d65921e38006138201cd4263fd8bb807d9dfc511074615cd264a571b3b1"}, "snowflake": {:hex, :snowflake, "1.0.4", "8433b4e04fbed19272c55e1b7de0f7a1ee1230b3ae31a813b616fd6ef279e87a", [:mix], [], "hexpm", "badb07ebb089a5cff737738297513db3962760b10fe2b158ae3bebf0b4d5be13"}, "sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"}, + "spitfire": {:git, "https://github.com/doorgan/spitfire.git", "79b5b25b9ee5a15a3b5b3544252e069076cba6cf", [branch: "doorgan/expert-bugs"]}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, diff --git a/apps/expert/mix.lock b/apps/expert/mix.lock index 2f2186f7..972c285e 100644 --- a/apps/expert/mix.lock +++ b/apps/expert/mix.lock @@ -24,6 +24,7 @@ "schematic": {:hex, :schematic, "0.2.1", "0b091df94146fd15a0a343d1bd179a6c5a58562527746dadd09477311698dbb1", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0b255d65921e38006138201cd4263fd8bb807d9dfc511074615cd264a571b3b1"}, "snowflake": {:hex, :snowflake, "1.0.4", "8433b4e04fbed19272c55e1b7de0f7a1ee1230b3ae31a813b616fd6ef279e87a", [:mix], [], "hexpm", "badb07ebb089a5cff737738297513db3962760b10fe2b158ae3bebf0b4d5be13"}, "sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"}, + "spitfire": {:git, "https://github.com/doorgan/spitfire.git", "79b5b25b9ee5a15a3b5b3544252e069076cba6cf", [branch: "doorgan/expert-bugs"]}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, } diff --git a/apps/forge/lib/forge/ast.ex b/apps/forge/lib/forge/ast.ex index 1a77ca5e..89070cab 100644 --- a/apps/forge/lib/forge/ast.ex +++ b/apps/forge/lib/forge/ast.ex @@ -83,8 +83,7 @@ defmodule Forge.Ast do @typedoc "Return value from `Code.Fragment.surround_context/3`" @type surround_context :: any() - @type parse_error :: - {location :: keyword(), String.t() | {String.t(), String.t()}, String.t()} + @type parse_error :: {location :: keyword(), String.t()} @type patch :: %{ optional(:preserve_indentation) => boolean(), @@ -486,21 +485,83 @@ defmodule Forge.Ast do # private defp do_string_to_quoted(string) when is_binary(string) do - Code.string_to_quoted_with_comments(string, - literal_encoder: &{:ok, {:__block__, &2, [&1]}}, - token_metadata: true, - columns: true, - unescape: false - ) + try do + Spitfire.parse_with_comments(string, + literal_encoder: &{:ok, {:__block__, &2, [&1]}}, + token_metadata: true, + columns: true, + unescape: false + ) + rescue + e in FunctionClauseError -> + {:error, {[line: 1, column: 1], "parser error: #{Exception.message(e)}"}, []} + + e in MatchError -> + {:error, {[line: 1, column: 1], "parser error: #{Exception.message(e)}"}, []} + + e in CaseClauseError -> + case e.term do + {:error, :no_fuel_remaining} -> + {:error, {[line: 1, column: 1], "parser exhausted fuel"}, []} + + _ -> + reraise e, __STACKTRACE__ + end + end + |> case do + {:ok, quoted, comments} -> + {:ok, quoted, comments} + + {:error, _quoted, comments, errors} -> + first = hd(errors) + {:error, first, comments} + + {:error, :no_fuel_remaining} -> + {:error, {[line: 1, column: 1], "parser exhausted fuel"}, []} + + {:error, {_location, _message}, _comments} = error -> + error + end end defp do_container_cursor_to_quoted(fragment) when is_binary(fragment) do - Code.Fragment.container_cursor_to_quoted(fragment, - literal_encoder: &{:ok, {:__block__, &2, [&1]}}, - token_metadata: true, - columns: true, - unescape: false - ) + try do + Spitfire.container_cursor_to_quoted(fragment, + literal_encoder: &{:ok, {:__block__, &2, [&1]}}, + token_metadata: true, + columns: true, + unescape: false + ) + rescue + e in FunctionClauseError -> + {:error, {[line: 1, column: 1], "parser error: #{Exception.message(e)}"}} + + e in MatchError -> + {:error, {[line: 1, column: 1], "parser error: #{Exception.message(e)}"}} + + e in CaseClauseError -> + case e.term do + {:error, :no_fuel_remaining} -> + {:error, {[line: 1, column: 1], "parser exhausted fuel"}} + + _ -> + reraise e, __STACKTRACE__ + end + end + |> case do + {:ok, quoted} -> + {:ok, quoted} + + {:error, _quoted, errors} -> + first = hd(errors) + {:error, first} + + {:error, :no_fuel_remaining} -> + {:error, {[line: 1, column: 1], "parser exhausted fuel"}} + + {:error, {_location, _message}} = error -> + error + end end defp do_cursor_context(fragment) when is_binary(fragment) do diff --git a/apps/forge/lib/test/detection_case.ex b/apps/forge/lib/test/detection_case.ex index e4961813..afa5d421 100644 --- a/apps/forge/lib/test/detection_case.ex +++ b/apps/forge/lib/test/detection_case.ex @@ -52,11 +52,11 @@ defmodule Forge.Test.DetectionCase do unquote_splicing(refutations) def assert_detected(code) do - assert_detected @context, code + assert_detected(@context, code) end def refute_detected(code) do - refute_detected @context, code + refute_detected(@context, code) end end end @@ -132,7 +132,7 @@ defmodule Forge.Test.DetectionCase do quote generated: true do test unquote(test_name) do - assert_detected unquote(context), unquote(assertion_text) + assert_detected(unquote(context), unquote(assertion_text)) end end end @@ -167,16 +167,16 @@ defmodule Forge.Test.DetectionCase do end defp build_refutation_variation(context, type, variation, test) do - {_range, refutation_text} = + {_ranges, refutation_text} = variation |> Variations.wrap_with(test) - |> pop_range() + |> pop_all_ranges() test_name = type_to_name(type, variation) quote generated: true do test unquote(test_name) do - refute_detected unquote(context), unquote(refutation_text) + refute_detected(unquote(context), unquote(refutation_text)) end end end diff --git a/apps/forge/mix.exs b/apps/forge/mix.exs index 9b32fed3..ba5877b4 100644 --- a/apps/forge/mix.exs +++ b/apps/forge/mix.exs @@ -42,6 +42,7 @@ defmodule Forge.MixProject do {:gen_lsp, "~> 0.11"}, {:snowflake, "~> 1.0"}, {:sourceror, "~> 1.9"}, + {:spitfire, github: "doorgan/spitfire", branch: "doorgan/expert-bugs"}, {:stream_data, "~> 1.1", only: [:test], runtime: false}, {:patch, "~> 0.15", only: [:test], optional: true, runtime: false} ] diff --git a/apps/forge/mix.lock b/apps/forge/mix.lock index 25c6c1eb..7a75dac8 100644 --- a/apps/forge/mix.lock +++ b/apps/forge/mix.lock @@ -16,6 +16,7 @@ "schematic": {:hex, :schematic, "0.2.1", "0b091df94146fd15a0a343d1bd179a6c5a58562527746dadd09477311698dbb1", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0b255d65921e38006138201cd4263fd8bb807d9dfc511074615cd264a571b3b1"}, "snowflake": {:hex, :snowflake, "1.0.4", "8433b4e04fbed19272c55e1b7de0f7a1ee1230b3ae31a813b616fd6ef279e87a", [:mix], [], "hexpm", "badb07ebb089a5cff737738297513db3962760b10fe2b158ae3bebf0b4d5be13"}, "sourceror": {:hex, :sourceror, "1.9.0", "3bf5fe2d017aaabe3866d8a6da097dd7c331e0d2d54e59e21c2b066d47f1e08e", [:mix], [], "hexpm", "d20a9dd5efe162f0d75a307146faa2e17b823ea4f134f662358d70f0332fed82"}, + "spitfire": {:git, "https://github.com/doorgan/spitfire.git", "79b5b25b9ee5a15a3b5b3544252e069076cba6cf", [branch: "doorgan/expert-bugs"]}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "stream_data": {:hex, :stream_data, "1.1.3", "15fdb14c64e84437901258bb56fc7d80aaf6ceaf85b9324f359e219241353bfb", [:mix], [], "hexpm", "859eb2be72d74be26c1c4f272905667672a52e44f743839c57c7ee73a1a66420"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, diff --git a/apps/forge/test/forge/ast_test.exs b/apps/forge/test/forge/ast_test.exs index fb978477..604c8535 100644 --- a/apps/forge/test/forge/ast_test.exs +++ b/apps/forge/test/forge/ast_test.exs @@ -44,13 +44,15 @@ defmodule Forge.AstTest do assert path == [{:__cursor__, [closing: [line: 1, column: 12], line: 1, column: 1], []}] end - test "returns [] when can't parse the AST" do + test "returns cursor path even for incomplete code" do text = ~q[ foo(bar do baz, [bat| ] path = cursor_path(text) - assert path == [] + # Spitfire's fault-tolerant parsing produces a cursor path + assert length(path) > 0 + assert Enum.any?(path, &match?({:__cursor__, _, _}, &1)) end end @@ -89,8 +91,9 @@ defmodule Forge.AstTest do defmodule |Foo do ] - assert {:error, {metadata, "missing terminator: end" <> _, ""}} = path_at(code) - assert end_location(metadata) == {2, 1} + assert {:error, {metadata, message}, _comments} = path_at(code) + assert is_list(metadata) + assert message =~ "missing" and message =~ "end" end test "returns a path to the innermost leaf at position" do @@ -309,7 +312,7 @@ defmodule Forge.AstTest do assert %Analysis{} = analysis = analyze(code) refute analysis.ast - assert {:error, _} = analysis.parse_error + assert {:error, {_location, _message}, _comments} = analysis.parse_error end test "creates an analysis from a document with incomplete `as` section" do